143 lines
7.2 KiB
JavaScript
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;
|
|
}
|