Files
pikasTech-unidesk/scripts/native/hwlab/runtime-pipeline-resource-governance.mjs
T
2026-07-21 08:53:31 +02:00

143 lines
7.2 KiB
JavaScript

// Injected into the HWLAB control-plane renderer. It intentionally uses the
// surrounding renderer's overlay, stepEnv, patchScript, YAML, and text values.
function patchPipelineTaskRuntime(task) {
const governance = overlay.ciResourceGovernance?.build;
const governed = governance && task.name === governance.matrixTaskName;
if (governed) task.timeout = governance.taskTimeout;
for (const sidecar of task.taskSpec?.sidecars || []) {
if (overlay.buildkitSidecarImage && typeof sidecar.image === 'string' && sidecar.image.includes('buildkit')) sidecar.image = overlay.buildkitSidecarImage;
if (governed) sidecar.computeResources = structuredClone(governance.sidecarResources);
}
for (const step of task.taskSpec?.steps || []) {
if (step.image === overlay.toolsImage && overlay.toolsImagePullPolicy) step.imagePullPolicy = overlay.toolsImagePullPolicy;
if (governed) step.computeResources = structuredClone(governance.stepResources);
if (Array.isArray(step.env)) {
for (const env of step.env) {
if (Object.prototype.hasOwnProperty.call(stepEnv, env.name) && stepEnv[env.name] !== undefined) env.value = stepEnv[env.name];
}
}
step.env = Array.isArray(step.env) ? step.env : [];
const existingEnv = new Set(step.env.map((env) => env.name));
for (const [name, value] of Object.entries(stepEnv)) {
if (value !== undefined && !existingEnv.has(name)) step.env.push({ name, value });
}
if (typeof step.script === 'string') step.script = patchScript(step.script);
}
}
function applyPipelineBuildQueue(doc) {
const governance = overlay.ciResourceGovernance?.build;
if (!governance) return;
const tasks = Array.isArray(doc.spec.tasks) ? doc.spec.tasks : [];
const task = tasks.find((candidate) => candidate?.name === governance.matrixTaskName);
if (!task) return;
const matrixParams = Array.isArray(task.matrix?.params) ? task.matrix.params : [];
const serviceParam = matrixParams.find((param) => param?.name === 'service-id');
if (!serviceParam || !Array.isArray(serviceParam.value) || serviceParam.value.length === 0) {
throw new Error(`CI resource governance task ${governance.matrixTaskName} must declare a static service-id matrix`);
}
const maxParallel = governance.maxParallelServices;
const publish = (task.taskSpec?.steps || []).find((step) => step?.name === 'publish');
if (!publish || typeof publish.script !== 'string') throw new Error(`CI resource governance task ${governance.matrixTaskName} must contain a publish script`);
const serviceIds = serviceParam.value.map((value) => String(value));
let workerScript = publish.script
.replaceAll('$(params.service-id)', '$service_id')
.replaceAll('/workspace/service-work', '/workspace/service-work/$service_id')
.replaceAll('"/tekton/results/" + name', 'process.env.HWLAB_TEKTON_RESULTS_DIR + "/" + name')
.replaceAll('--tekton-results-dir /tekton/results', '--tekton-results-dir "$HWLAB_TEKTON_RESULTS_DIR"');
if (workerScript.includes('/tekton/results')) {
throw new Error(`CI resource governance task ${governance.matrixTaskName} contains an unsupported Tekton result path context`);
}
publish.script = [
'set -eu',
'build_service() {',
' service_id="$1"',
' mkdir -p "/workspace/service-work/$service_id" "/workspace/service-results-tekton/$service_id"',
' export HWLAB_TEKTON_RESULTS_DIR="/workspace/service-results-tekton/$service_id"',
// Keep the worker body at column zero: POSIX heredoc terminators in the
// generated publish script must not be indented by the function wrapper.
workerScript,
'}',
`max_parallel=${maxParallel}`,
'service_csv="$(params.services)"',
'old_ifs="$IFS"',
"IFS=','",
'set -- $service_csv',
'IFS="$old_ifs"',
'failed=0',
'while [ "$#" -gt 0 ]; do',
' pids=""',
' launched=0',
' while [ "$#" -gt 0 ] && [ "$launched" -lt "$max_parallel" ]; do',
' service_id="$1"',
' shift',
' (build_service "$service_id") &',
' pids="$pids $!"',
' launched=$((launched + 1))',
' done',
' for pid in $pids; do if ! wait "$pid"; then failed=1; fi; done',
' if [ "$failed" -ne 0 ]; then exit "$failed"; fi',
'done',
].join('\n');
task.params = (task.params || []).map((param) => param?.name === 'service-id' ? { name: 'services', value: '$(params.services)' } : param);
if (!task.params.some((param) => param?.name === 'services')) task.params.push({ name: 'services', value: '$(params.services)' });
task.taskSpec.params = (task.taskSpec.params || []).map((param) => param?.name === 'service-id' ? { ...param, name: 'services' } : param);
task.taskSpec.results = [];
delete task.matrix;
doc.metadata.annotations['unidesk.ai/ci-build-queue-mode'] = 'single-taskrun-bounded-workers';
doc.metadata.annotations['unidesk.ai/ci-build-service-count'] = String(serviceIds.length);
doc.metadata.annotations['unidesk.ai/ci-build-batch-count'] = String(Math.ceil(serviceIds.length / maxParallel));
doc.metadata.annotations['unidesk.ai/ci-build-max-parallel'] = String(maxParallel);
}
function patchManifestObject(doc) {
if (!doc || typeof doc !== 'object') return false;
if (doc.kind !== 'Pipeline' || !doc.spec) return false;
const defaults = {
'git-url': overlay.gitUrl,
'git-read-url': overlay.gitReadUrl,
'git-write-url': overlay.gitWriteUrl,
'catalog-path': overlay.catalogPath,
'runtime-path': overlay.runtimePath,
'registry-prefix': overlay.registryPrefix,
};
for (const param of doc.spec?.params || []) {
if (Object.prototype.hasOwnProperty.call(defaults, param.name)) param.default = defaults[param.name];
}
doc.metadata = doc.metadata || {};
if (typeof overlay.pipelineName === 'string' && overlay.pipelineName.length > 0) doc.metadata.name = overlay.pipelineName;
doc.metadata.annotations = doc.metadata.annotations || {};
doc.metadata.annotations['hwlab.pikastech.local/download-profile'] = overlay.downloadProfileId;
doc.metadata.annotations['hwlab.pikastech.local/network-profile'] = overlay.networkProfileId;
if (overlay.ciResourceGovernance) {
doc.metadata.annotations['unidesk.ai/ci-resource-governance-config-ref'] = overlay.ciResourceGovernance.configRef;
doc.metadata.annotations['unidesk.ai/ci-dedupe-identity'] = overlay.ciResourceGovernance.dedupe.identityFields.join('+');
}
for (const task of doc.spec?.tasks || []) patchPipelineTaskRuntime(task);
applyPipelineBuildQueue(doc);
return true;
}
function patchStructuredPipeline() {
try {
const doc = JSON.parse(text);
if (!patchManifestObject(doc)) return false;
text = JSON.stringify(doc, null, 2) + '\n';
return true;
} catch (error) {
if (String(error?.message || '').includes('CI resource governance')) throw error;
}
if (YAML) {
try {
const docs = YAML.parseAllDocuments(text).map((document) => document.toJS()).filter((doc) => doc !== null);
const changed = docs.some((doc) => patchManifestObject(doc));
if (!changed) return false;
text = docs.map((doc) => YAML.stringify(doc).trimEnd()).join('\n---\n') + '\n';
return true;
} catch (error) {
if (String(error?.message || '').includes('CI resource governance')) throw error;
}
}
return false;
}