|
|
|
@@ -693,6 +693,10 @@ for (const line of lines) {
|
|
|
|
|
}
|
|
|
|
|
const publish = [...records].reverse().find((item) => item.phase === 'gitops-publish' || item.gitopsCommit);
|
|
|
|
|
const image = publish || [...records].reverse().find((item) => item.imageStatus || item.status === 'reused' || item.status === 'built');
|
|
|
|
|
const gitopsCommit = image?.gitopsCommit
|
|
|
|
|
|| [...lines].reverse().map((line) => line.match(/^\[[^\]]+\s+([0-9a-f]{7,64})\]/u)?.[1] || null).find(Boolean)
|
|
|
|
|
|| null;
|
|
|
|
|
const loggedDigests = [...new Set(lines.flatMap((line) => [...line.matchAll(/"(?:digest|environmentDigest)"\s*:\s*"(sha256:[0-9a-f]{64})"/gu)].map((match) => match[1])))];
|
|
|
|
|
const envHeaderIndex = lines.findIndex((line) => /^ENV_REUSE\s+NODE_DEPS\s+/u.test(line.trim()));
|
|
|
|
|
let humanEnv = null;
|
|
|
|
|
if (envHeaderIndex >= 0) {
|
|
|
|
@@ -719,22 +723,24 @@ if (image) {
|
|
|
|
|
envReuse: envReuseOf(image),
|
|
|
|
|
nodeDepsReuse: null,
|
|
|
|
|
buildCache: null,
|
|
|
|
|
digest: digestOf(image),
|
|
|
|
|
gitopsCommit: image.gitopsCommit || null,
|
|
|
|
|
digest: digestOf(image) || (loggedDigests.length === 1 ? loggedDigests[0] : null),
|
|
|
|
|
digests: loggedDigests,
|
|
|
|
|
gitopsCommit,
|
|
|
|
|
sourceCommit: image.sourceCommit || null,
|
|
|
|
|
valuesPrinted: false,
|
|
|
|
|
};
|
|
|
|
|
} else if (humanEnv) {
|
|
|
|
|
} else if (humanEnv || gitopsCommit || loggedDigests.length > 0) {
|
|
|
|
|
out = {
|
|
|
|
|
imageStatus: 'published',
|
|
|
|
|
imageStatus: humanEnv ? 'published' : null,
|
|
|
|
|
envIdentity: null,
|
|
|
|
|
envReuse: humanEnv.envReuse,
|
|
|
|
|
nodeDepsReuse: humanEnv.nodeDeps,
|
|
|
|
|
buildCache: humanEnv.cache,
|
|
|
|
|
buildPackage: humanEnv.buildPackage,
|
|
|
|
|
buildNetwork: humanEnv.buildNetwork,
|
|
|
|
|
digest: null,
|
|
|
|
|
gitopsCommit: null,
|
|
|
|
|
envReuse: humanEnv?.envReuse || null,
|
|
|
|
|
nodeDepsReuse: humanEnv?.nodeDeps || null,
|
|
|
|
|
buildCache: humanEnv?.cache || null,
|
|
|
|
|
buildPackage: humanEnv?.buildPackage || null,
|
|
|
|
|
buildNetwork: humanEnv?.buildNetwork || null,
|
|
|
|
|
digest: loggedDigests.length === 1 ? loggedDigests[0] : null,
|
|
|
|
|
digests: loggedDigests,
|
|
|
|
|
gitopsCommit,
|
|
|
|
|
sourceCommit: null,
|
|
|
|
|
valuesPrinted: false,
|
|
|
|
|
};
|
|
|
|
@@ -795,6 +801,124 @@ NODE
|
|
|
|
|
rm -f "$deploy_file"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resolve_git_service_url() {
|
|
|
|
|
repo_url="$1"
|
|
|
|
|
host=$(UNIDESK_PAC_GIT_URL="$repo_url" node <<'NODE'
|
|
|
|
|
try {
|
|
|
|
|
process.stdout.write(new URL(process.env.UNIDESK_PAC_GIT_URL || '').hostname);
|
|
|
|
|
} catch {}
|
|
|
|
|
NODE
|
|
|
|
|
)
|
|
|
|
|
case "$host" in
|
|
|
|
|
*.svc.cluster.local)
|
|
|
|
|
service=${host%%.*}
|
|
|
|
|
rest=${host#*.}
|
|
|
|
|
namespace=${rest%%.*}
|
|
|
|
|
cluster_ip=$(kubectl -n "$namespace" get svc "$service" -o jsonpath='{.spec.clusterIP}' 2>/dev/null || true)
|
|
|
|
|
if [ -z "$cluster_ip" ]; then
|
|
|
|
|
return 1
|
|
|
|
|
fi
|
|
|
|
|
UNIDESK_PAC_GIT_URL="$repo_url" UNIDESK_PAC_GIT_CLUSTER_IP="$cluster_ip" node <<'NODE'
|
|
|
|
|
try {
|
|
|
|
|
const url = new URL(process.env.UNIDESK_PAC_GIT_URL || '');
|
|
|
|
|
url.hostname = process.env.UNIDESK_PAC_GIT_CLUSTER_IP || '';
|
|
|
|
|
process.stdout.write(url.toString());
|
|
|
|
|
} catch {
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
NODE
|
|
|
|
|
;;
|
|
|
|
|
*) printf '%s' "$repo_url" ;;
|
|
|
|
|
esac
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
gitops_revision_relation() {
|
|
|
|
|
artifact_json="${1:-}"
|
|
|
|
|
argo_json="${2:-}"
|
|
|
|
|
[ -n "$artifact_json" ] || artifact_json='{}'
|
|
|
|
|
[ -n "$argo_json" ] || argo_json='{}'
|
|
|
|
|
fields=$(UNIDESK_PAC_ARTIFACT_JSON="$artifact_json" UNIDESK_PAC_ARGO_JSON="$argo_json" node <<'NODE'
|
|
|
|
|
function parse(value) {
|
|
|
|
|
try { return JSON.parse(value || '{}'); } catch { return {}; }
|
|
|
|
|
}
|
|
|
|
|
function line(key, value) {
|
|
|
|
|
process.stdout.write(`${key}=${Buffer.from(String(value || ''), 'utf8').toString('base64')}\n`);
|
|
|
|
|
}
|
|
|
|
|
const artifact = parse(process.env.UNIDESK_PAC_ARTIFACT_JSON);
|
|
|
|
|
const argo = parse(process.env.UNIDESK_PAC_ARGO_JSON);
|
|
|
|
|
line('expected', artifact.gitopsCommit);
|
|
|
|
|
line('observed', argo.revision);
|
|
|
|
|
line('repoUrl', argo.repoURL);
|
|
|
|
|
line('targetRevision', argo.targetRevision);
|
|
|
|
|
NODE
|
|
|
|
|
)
|
|
|
|
|
expected=$(printf '%s\n' "$fields" | sed -n 's/^expected=//p' | base64 -d 2>/dev/null || true)
|
|
|
|
|
observed=$(printf '%s\n' "$fields" | sed -n 's/^observed=//p' | base64 -d 2>/dev/null || true)
|
|
|
|
|
repo_url=$(printf '%s\n' "$fields" | sed -n 's/^repoUrl=//p' | base64 -d 2>/dev/null || true)
|
|
|
|
|
target_revision=$(printf '%s\n' "$fields" | sed -n 's/^targetRevision=//p' | base64 -d 2>/dev/null || true)
|
|
|
|
|
relation=unknown
|
|
|
|
|
proof=unavailable
|
|
|
|
|
reason=missing-revision-context
|
|
|
|
|
fetch_ok=false
|
|
|
|
|
if printf '%s\n%s\n' "$expected" "$observed" | grep -Eqv '^[0-9a-fA-F]{7,64}$'; then
|
|
|
|
|
reason=invalid-or-missing-revision
|
|
|
|
|
elif [ -z "$repo_url" ] \
|
|
|
|
|
|| ! git check-ref-format "refs/heads/$target_revision" >/dev/null 2>&1; then
|
|
|
|
|
reason=invalid-or-missing-git-context
|
|
|
|
|
else
|
|
|
|
|
repo_dir=$(mktemp -d)
|
|
|
|
|
if resolved_url=$(resolve_git_service_url "$repo_url") \
|
|
|
|
|
&& git init --bare -q "$repo_dir/repo.git" \
|
|
|
|
|
&& timeout "$UNIDESK_PAC_WAIT_TIMEOUT_SECONDS" git --git-dir="$repo_dir/repo.git" fetch -q --no-tags "$resolved_url" "+refs/heads/$target_revision:refs/remotes/origin/$target_revision" >/dev/null 2>&1; then
|
|
|
|
|
fetch_ok=true
|
|
|
|
|
if git --git-dir="$repo_dir/repo.git" cat-file -e "$expected^{commit}" 2>/dev/null \
|
|
|
|
|
&& git --git-dir="$repo_dir/repo.git" cat-file -e "$observed^{commit}" 2>/dev/null; then
|
|
|
|
|
proof=owning-git-commit-graph
|
|
|
|
|
expected=$(git --git-dir="$repo_dir/repo.git" rev-parse "$expected^{commit}")
|
|
|
|
|
observed=$(git --git-dir="$repo_dir/repo.git" rev-parse "$observed^{commit}")
|
|
|
|
|
if [ "$expected" = "$observed" ]; then
|
|
|
|
|
relation=exact
|
|
|
|
|
reason=commits-equal
|
|
|
|
|
elif git --git-dir="$repo_dir/repo.git" merge-base --is-ancestor "$expected" "$observed" >/dev/null 2>&1; then
|
|
|
|
|
relation=descendant
|
|
|
|
|
reason=observed-descends-from-selected
|
|
|
|
|
elif git --git-dir="$repo_dir/repo.git" merge-base --is-ancestor "$observed" "$expected" >/dev/null 2>&1; then
|
|
|
|
|
relation=stale
|
|
|
|
|
reason=observed-precedes-selected
|
|
|
|
|
else
|
|
|
|
|
relation=diverged
|
|
|
|
|
reason=commits-have-diverged
|
|
|
|
|
fi
|
|
|
|
|
else
|
|
|
|
|
reason=commit-object-missing
|
|
|
|
|
fi
|
|
|
|
|
else
|
|
|
|
|
reason=owning-git-fetch-failed
|
|
|
|
|
fi
|
|
|
|
|
rm -rf "$repo_dir"
|
|
|
|
|
fi
|
|
|
|
|
UNIDESK_PAC_GITOPS_EXPECTED="$expected" \
|
|
|
|
|
UNIDESK_PAC_GITOPS_OBSERVED="$observed" \
|
|
|
|
|
UNIDESK_PAC_GITOPS_REPO_URL="$repo_url" \
|
|
|
|
|
UNIDESK_PAC_GITOPS_TARGET_REVISION="$target_revision" \
|
|
|
|
|
UNIDESK_PAC_GITOPS_RELATION="$relation" \
|
|
|
|
|
UNIDESK_PAC_GITOPS_PROOF="$proof" \
|
|
|
|
|
UNIDESK_PAC_GITOPS_REASON="$reason" \
|
|
|
|
|
UNIDESK_PAC_GITOPS_FETCH_OK="$fetch_ok" node <<'NODE'
|
|
|
|
|
process.stdout.write(JSON.stringify({
|
|
|
|
|
relation: process.env.UNIDESK_PAC_GITOPS_RELATION || 'unknown',
|
|
|
|
|
expectedRevision: process.env.UNIDESK_PAC_GITOPS_EXPECTED || null,
|
|
|
|
|
observedRevision: process.env.UNIDESK_PAC_GITOPS_OBSERVED || null,
|
|
|
|
|
repoURL: process.env.UNIDESK_PAC_GITOPS_REPO_URL || null,
|
|
|
|
|
targetRevision: process.env.UNIDESK_PAC_GITOPS_TARGET_REVISION || null,
|
|
|
|
|
proof: process.env.UNIDESK_PAC_GITOPS_PROOF || 'unavailable',
|
|
|
|
|
reason: process.env.UNIDESK_PAC_GITOPS_REASON || null,
|
|
|
|
|
fetchOk: process.env.UNIDESK_PAC_GITOPS_FETCH_OK === 'true',
|
|
|
|
|
valuesPrinted: false,
|
|
|
|
|
}));
|
|
|
|
|
NODE
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cicd_diagnostics() {
|
|
|
|
|
params_file=$(mktemp)
|
|
|
|
|
pipelines_file=$(mktemp)
|
|
|
|
@@ -926,12 +1050,17 @@ const expectedDigest = registryDigest || artifactDigest;
|
|
|
|
|
const runtimeImage = runtime.image || null;
|
|
|
|
|
const runtimeDigest = runtime.digest || (typeof runtimeImage === 'string' && runtimeImage.includes('@') ? runtimeImage.split('@').slice(1).join('@') : null);
|
|
|
|
|
const runtimeMatches = !expectedDigest || runtimeDigest === expectedDigest;
|
|
|
|
|
const runtimeReady = Number.isInteger(runtime.replicas) && runtime.replicas > 0 && runtime.readyReplicas === runtime.replicas;
|
|
|
|
|
const selectedArtifactDigests = [...new Set([artifactDigest, ...(Array.isArray(artifact.digests) ? artifact.digests : [])].filter(Boolean))];
|
|
|
|
|
const selectedArtifactMatchesRuntime = Boolean(runtimeDigest && selectedArtifactDigests.includes(runtimeDigest));
|
|
|
|
|
const registryMatchesArtifact = !registryDigest || !artifactDigest || registryDigest === artifactDigest;
|
|
|
|
|
const healthUrl = process.env.UNIDESK_PAC_DIAG_HEALTH_URL || null;
|
|
|
|
|
const healthStatus = process.env.UNIDESK_PAC_DIAG_HEALTH_STATUS || null;
|
|
|
|
|
const healthReady = !healthUrl || healthStatus === '200';
|
|
|
|
|
const gitopsReady = Boolean(artifact.gitopsCommit || argo.revision);
|
|
|
|
|
const gitopsRevisionMatches = !artifact.gitopsCommit || artifact.gitopsCommit === argo.revision;
|
|
|
|
|
const revisionRelation = argo.revisionRelation || { relation: 'unknown' };
|
|
|
|
|
const gitopsRelation = revisionRelation.relation || 'unknown';
|
|
|
|
|
const gitopsReady = Boolean(artifact.gitopsCommit && argo.revision);
|
|
|
|
|
const gitopsRevisionAligned = gitopsRelation === 'exact' || gitopsRelation === 'descendant';
|
|
|
|
|
const deliveryDisabled = artifact.imageStatus === 'disabled';
|
|
|
|
|
let code = 'pac-diagnostics-not-applicable';
|
|
|
|
|
let phase = 'not-applicable';
|
|
|
|
@@ -949,14 +1078,22 @@ if (deliveryDisabled) {
|
|
|
|
|
ok = false; code = 'pac-gitops-missing'; phase = 'artifact-ready-gitops-missing'; hint = 'PipelineRun completed but its GitOps commit is not visible';
|
|
|
|
|
} else if (argo.sync !== 'Synced' || argo.health !== 'Healthy') {
|
|
|
|
|
ok = false; code = 'pac-argo-not-ready'; phase = 'gitops-ready-argo-pending'; hint = 'GitOps exists but Argo is not Synced/Healthy';
|
|
|
|
|
} else if (!gitopsRevisionMatches) {
|
|
|
|
|
ok = false; code = 'pac-argo-revision-not-aligned'; phase = 'gitops-ready-argo-revision-mismatch'; hint = 'Argo has not observed the GitOps commit produced by the selected PipelineRun';
|
|
|
|
|
} else if (!gitopsRevisionAligned) {
|
|
|
|
|
ok = false; code = `pac-argo-revision-${gitopsRelation}`; phase = `gitops-ready-argo-revision-${gitopsRelation}`; hint = `Argo revision relation is ${gitopsRelation}; only exact or commit-graph-proven descendant is deployable`;
|
|
|
|
|
} else if (gitopsRelation === 'descendant' && !selectedArtifactMatchesRuntime) {
|
|
|
|
|
ok = false; code = 'pac-descendant-artifact-runtime-mismatch'; phase = 'argo-descendant-runtime-mismatch'; hint = 'a GitOps descendant is ready only when the selected PipelineRun artifact digest exactly matches the runtime digest';
|
|
|
|
|
} else if (!runtimeMatches) {
|
|
|
|
|
ok = false; code = 'pac-runtime-not-aligned'; phase = 'argo-ready-runtime-mismatch'; hint = 'runtime image digest does not match the artifact digest produced by the selected PipelineRun';
|
|
|
|
|
} else if (!runtimeReady) {
|
|
|
|
|
ok = false; code = 'pac-runtime-not-ready'; phase = 'runtime-replicas-not-ready'; hint = 'runtime ready replicas do not match the desired replica count';
|
|
|
|
|
} else if (!healthReady) {
|
|
|
|
|
ok = false; code = 'pac-health-not-ready'; phase = 'runtime-ready-health-pending'; hint = 'runtime image is aligned but the configured health endpoint is not ready';
|
|
|
|
|
} else if (artifactDigest || process.env.UNIDESK_PAC_DIAG_IMAGE_REPOSITORY) {
|
|
|
|
|
code = 'pac-ready'; phase = 'ready'; hint = 'source, artifact, GitOps, Argo and runtime are aligned';
|
|
|
|
|
} else if (artifactDigest || selectedArtifactDigests.length > 0 || process.env.UNIDESK_PAC_DIAG_IMAGE_REPOSITORY) {
|
|
|
|
|
code = gitopsRelation === 'descendant' ? 'pac-ready-gitops-descendant' : 'pac-ready-gitops-exact';
|
|
|
|
|
phase = 'ready';
|
|
|
|
|
hint = gitopsRelation === 'descendant'
|
|
|
|
|
? 'Argo is on a commit-graph-proven GitOps descendant and the selected artifact, runtime, Argo and health are aligned'
|
|
|
|
|
: 'Argo is on the exact selected GitOps commit and the artifact, runtime and health are aligned';
|
|
|
|
|
}
|
|
|
|
|
process.stdout.write(JSON.stringify({
|
|
|
|
|
ok,
|
|
|
|
@@ -972,12 +1109,21 @@ process.stdout.write(JSON.stringify({
|
|
|
|
|
digest: registryDigest,
|
|
|
|
|
url: process.env.UNIDESK_PAC_DIAG_REGISTRY_URL || null,
|
|
|
|
|
},
|
|
|
|
|
selectedArtifactDigests,
|
|
|
|
|
gitops: {
|
|
|
|
|
branch: process.env.UNIDESK_PAC_DIAG_GITOPS_BRANCH || null,
|
|
|
|
|
manifestPath: process.env.UNIDESK_PAC_DIAG_GITOPS_MANIFEST_PATH || null,
|
|
|
|
|
commit: artifact.gitopsCommit || argo.revision || null,
|
|
|
|
|
revisionRelation,
|
|
|
|
|
},
|
|
|
|
|
argo: {
|
|
|
|
|
sync: argo.sync || null,
|
|
|
|
|
health: argo.health || null,
|
|
|
|
|
revision: argo.revision || null,
|
|
|
|
|
repoURL: argo.repoURL || null,
|
|
|
|
|
targetRevision: argo.targetRevision || null,
|
|
|
|
|
revisionRelation,
|
|
|
|
|
},
|
|
|
|
|
argo: { sync: argo.sync || null, health: argo.health || null, revision: argo.revision || null },
|
|
|
|
|
runtime: { image: runtimeImage, digest: runtimeDigest, readyReplicas: runtime.readyReplicas ?? null, replicas: runtime.replicas ?? null },
|
|
|
|
|
health: { url: healthUrl, status: healthStatus, ready: healthReady },
|
|
|
|
|
pipelineRun: latest.name || null,
|
|
|
|
@@ -1008,8 +1154,21 @@ status_action() {
|
|
|
|
|
tasks=$(task_rows)
|
|
|
|
|
artifact=$(json_normalize "$(artifact_summary)")
|
|
|
|
|
hooks=$(hook_summary)
|
|
|
|
|
argo=$(kubectl -n "$UNIDESK_PAC_ARGO_NAMESPACE" get application "$UNIDESK_PAC_ARGO_APPLICATION" -o json 2>/dev/null | node -e 'const fs=require("fs"); const s=fs.readFileSync(0,"utf8").trim(); if(!s){process.stdout.write("{}"); process.exit(0)} const a=JSON.parse(s); process.stdout.write(JSON.stringify({sync:a.status?.sync?.status||null, health:a.status?.health?.status||null, revision:a.status?.sync?.revision||null}))' || echo '{}')
|
|
|
|
|
argo=$(kubectl -n "$UNIDESK_PAC_ARGO_NAMESPACE" get application "$UNIDESK_PAC_ARGO_APPLICATION" -o json 2>/dev/null | node -e 'const fs=require("fs"); const s=fs.readFileSync(0,"utf8").trim(); if(!s){process.stdout.write("{}"); process.exit(0)} const a=JSON.parse(s); const source=a.spec?.source || a.spec?.sources?.[0] || {}; let repoURL=source.repoURL||null; try { const url=new URL(repoURL); url.username=""; url.password=""; repoURL=url.toString(); } catch {} process.stdout.write(JSON.stringify({sync:a.status?.sync?.status||null, health:a.status?.health?.status||null, revision:a.status?.sync?.revision||null, repoURL, targetRevision:source.targetRevision||null}))' || echo '{}')
|
|
|
|
|
argo=$(json_normalize "$argo")
|
|
|
|
|
revision_relation=$(gitops_revision_relation "$artifact" "$argo")
|
|
|
|
|
artifact=$(UNIDESK_PAC_ARTIFACT_JSON="$artifact" UNIDESK_PAC_REVISION_RELATION_JSON="$revision_relation" node <<'NODE'
|
|
|
|
|
const artifact = JSON.parse(process.env.UNIDESK_PAC_ARTIFACT_JSON || '{}');
|
|
|
|
|
const revisionRelation = JSON.parse(process.env.UNIDESK_PAC_REVISION_RELATION_JSON || '{}');
|
|
|
|
|
process.stdout.write(JSON.stringify({ ...artifact, gitopsCommit: revisionRelation.expectedRevision || artifact.gitopsCommit || null }));
|
|
|
|
|
NODE
|
|
|
|
|
)
|
|
|
|
|
argo=$(UNIDESK_PAC_ARGO_JSON="$argo" UNIDESK_PAC_REVISION_RELATION_JSON="$revision_relation" node <<'NODE'
|
|
|
|
|
const argo = JSON.parse(process.env.UNIDESK_PAC_ARGO_JSON || '{}');
|
|
|
|
|
const revisionRelation = JSON.parse(process.env.UNIDESK_PAC_REVISION_RELATION_JSON || '{"relation":"unknown"}');
|
|
|
|
|
process.stdout.write(JSON.stringify({ ...argo, revisionRelation }));
|
|
|
|
|
NODE
|
|
|
|
|
)
|
|
|
|
|
runtime=$(json_normalize "$(runtime_summary)")
|
|
|
|
|
diagnostics=$(cicd_diagnostics "$pipelines" "$artifact" "$argo" "$runtime")
|
|
|
|
|
printf '{"ok":%s,"crdPresent":%s,"controllerReady":"%s","repository":{"name":"%s","repo":"%s/%s","url":"%s","condition":"%s"},"repositoryCondition":"%s","webhooks":%s,"pipelineRuns":%s,"taskRuns":%s,"artifact":%s,"argo":%s,"runtime":%s,"diagnostics":%s,"valuesPrinted":false}\n' \
|
|
|
|
|