1eafb1f89e
从目标 TaskRun condition/result 与结构化合同日志恢复 artifact catalog、digest 和 GitOps 证据,并为真实 PipelineRun 增加有界 debug-step。严格保留缺 artifact、缺 GitOps、runtime mismatch 与冲突证据的 fail-closed closeout。
1335 lines
59 KiB
Bash
1335 lines
59 KiB
Bash
#!/bin/sh
|
|
set -eu
|
|
|
|
UNIDESK_PAC_EVALUATOR_PATH=$(mktemp)
|
|
printf '%s' "$UNIDESK_PAC_EVALUATOR_B64" | base64 -d >"$UNIDESK_PAC_EVALUATOR_PATH"
|
|
export UNIDESK_PAC_EVALUATOR_PATH
|
|
trap 'rm -f "$UNIDESK_PAC_EVALUATOR_PATH"' 0 1 2 15
|
|
|
|
json_escape() {
|
|
sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g' | tr '\n' ' '
|
|
}
|
|
|
|
json_string() {
|
|
printf '%s' "$1" | json_escape
|
|
}
|
|
|
|
now_epoch() {
|
|
date +%s
|
|
}
|
|
|
|
duration_seconds() {
|
|
start="$1"
|
|
end="$2"
|
|
if [ -n "$start" ] && [ -n "$end" ]; then
|
|
s=$(date -d "$start" +%s 2>/dev/null || echo "")
|
|
e=$(date -d "$end" +%s 2>/dev/null || echo "")
|
|
if [ -n "$s" ] && [ -n "$e" ]; then
|
|
echo $((e - s))
|
|
return
|
|
fi
|
|
fi
|
|
echo null
|
|
}
|
|
|
|
json_normalize() {
|
|
UNIDESK_JSON_NORMALIZE_INPUT="${1:-}" node <<'NODE'
|
|
const input = String(process.env.UNIDESK_JSON_NORMALIZE_INPUT || '').trim();
|
|
if (!input) {
|
|
process.stdout.write('{}');
|
|
process.exit(0);
|
|
}
|
|
for (let end = input.length; end > 0; end -= 1) {
|
|
try {
|
|
process.stdout.write(JSON.stringify(JSON.parse(input.slice(0, end))));
|
|
process.exit(0);
|
|
} catch {}
|
|
}
|
|
process.stdout.write('{}');
|
|
NODE
|
|
}
|
|
|
|
api_url() {
|
|
prepare_gitea_api_base
|
|
printf '%s/api/v1/%s' "$UNIDESK_PAC_GITEA_API_BASE_URL" "$1"
|
|
}
|
|
|
|
prepare_gitea_api_base() {
|
|
if [ -n "${UNIDESK_PAC_GITEA_API_BASE_URL:-}" ]; then
|
|
return
|
|
fi
|
|
base="$UNIDESK_PAC_GITEA_BASE_URL"
|
|
hostport=$(printf '%s' "$base" | sed -n 's#^http://\([^/]*\).*$#\1#p')
|
|
if printf '%s' "$hostport" | grep -q '\.svc\.cluster\.local'; then
|
|
host=${hostport%%:*}
|
|
port=${hostport##*:}
|
|
if [ "$port" = "$hostport" ]; then port=80; fi
|
|
service=${host%%.*}
|
|
rest=${host#*.}
|
|
namespace=${rest%%.*}
|
|
cluster_ip=$(kubectl -n "$namespace" get svc "$service" -o jsonpath='{.spec.clusterIP}')
|
|
UNIDESK_PAC_GITEA_API_BASE_URL="http://$cluster_ip:$port"
|
|
else
|
|
UNIDESK_PAC_GITEA_API_BASE_URL="$base"
|
|
fi
|
|
export UNIDESK_PAC_GITEA_API_BASE_URL
|
|
}
|
|
|
|
gitea_api() {
|
|
method="$1"
|
|
path="$2"
|
|
body="${3:-}"
|
|
tmp=$(mktemp)
|
|
if [ -n "$body" ]; then
|
|
code=$(curl -sS -o "$tmp" -w '%{http_code}' -u "$UNIDESK_PAC_GITEA_ADMIN_USERNAME:$UNIDESK_PAC_GITEA_ADMIN_PASSWORD" \
|
|
-H 'Content-Type: application/json' \
|
|
-X "$method" \
|
|
--data "$body" \
|
|
"$(api_url "$path")" || true)
|
|
else
|
|
code=$(curl -sS -o "$tmp" -w '%{http_code}' -u "$UNIDESK_PAC_GITEA_ADMIN_USERNAME:$UNIDESK_PAC_GITEA_ADMIN_PASSWORD" \
|
|
-X "$method" \
|
|
"$(api_url "$path")" || true)
|
|
fi
|
|
case "$code" in
|
|
2*) cat "$tmp"; rm -f "$tmp"; return 0 ;;
|
|
esac
|
|
response=$(head -c 500 "$tmp" | tr '\n' ' ' | sed 's/[[:space:]][[:space:]]*/ /g')
|
|
rm -f "$tmp"
|
|
printf 'gitea api %s %s failed: HTTP %s response=%s\n' "$method" "$path" "$code" "$response" >&2
|
|
return 22
|
|
}
|
|
|
|
ensure_token() {
|
|
name="unidesk-pac-$(date +%Y%m%d%H%M%S)"
|
|
body=$(printf '{"name":"%s","scopes":["all"]}' "$name")
|
|
out=$(gitea_api POST "users/$UNIDESK_PAC_GITEA_API_USERNAME/tokens" "$body")
|
|
printf '%s' "$out" | sed -n 's/.*"sha1"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p'
|
|
}
|
|
|
|
ensure_webhook() {
|
|
hooks=$(gitea_api GET "repos/$UNIDESK_PAC_GITEA_OWNER/$UNIDESK_PAC_GITEA_REPO/hooks")
|
|
hook_ids=$(HOOKS_JSON="$hooks" node <<'NODE'
|
|
const data = JSON.parse(process.env.HOOKS_JSON || '[]');
|
|
const url = process.env.UNIDESK_PAC_WEBHOOK_URL;
|
|
for (const item of data) if (item?.config?.url === url && item?.id) console.log(item.id);
|
|
NODE
|
|
)
|
|
hook_id=$(printf '%s\n' "$hook_ids" | sed -n '1p')
|
|
body=$(printf '{"type":"gitea","config":{"url":"%s","content_type":"json","secret":"%s"},"events":["push"],"active":true}' "$UNIDESK_PAC_WEBHOOK_URL" "$UNIDESK_PAC_WEBHOOK_SECRET")
|
|
if [ -n "$hook_id" ]; then
|
|
if ! gitea_api PATCH "repos/$UNIDESK_PAC_GITEA_OWNER/$UNIDESK_PAC_GITEA_REPO/hooks/$hook_id" "$body" >/dev/null; then
|
|
gitea_api DELETE "repos/$UNIDESK_PAC_GITEA_OWNER/$UNIDESK_PAC_GITEA_REPO/hooks/$hook_id" >/dev/null || true
|
|
created=$(gitea_api POST "repos/$UNIDESK_PAC_GITEA_OWNER/$UNIDESK_PAC_GITEA_REPO/hooks" "$body")
|
|
hook_id=$(printf '%s' "$created" | sed -n 's/.*"id"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p')
|
|
fi
|
|
printf '%s\n' "$hook_ids" | sed -n '2,$p' | while IFS= read -r duplicate_id; do
|
|
[ -n "$duplicate_id" ] || continue
|
|
gitea_api DELETE "repos/$UNIDESK_PAC_GITEA_OWNER/$UNIDESK_PAC_GITEA_REPO/hooks/$duplicate_id" >/dev/null || true
|
|
done
|
|
else
|
|
created=$(gitea_api POST "repos/$UNIDESK_PAC_GITEA_OWNER/$UNIDESK_PAC_GITEA_REPO/hooks" "$body")
|
|
hook_id=$(printf '%s' "$created" | sed -n 's/.*"id"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p')
|
|
fi
|
|
printf '%s' "$hook_id"
|
|
}
|
|
|
|
repository_manifest() {
|
|
cat <<EOF
|
|
apiVersion: pipelinesascode.tekton.dev/v1alpha1
|
|
kind: Repository
|
|
metadata:
|
|
name: $UNIDESK_PAC_REPOSITORY_NAME
|
|
namespace: $UNIDESK_PAC_TARGET_NAMESPACE
|
|
labels:
|
|
app.kubernetes.io/managed-by: unidesk
|
|
app.kubernetes.io/part-of: $UNIDESK_PAC_PART_OF
|
|
unidesk.ai/spec: $UNIDESK_PAC_SPEC
|
|
spec:
|
|
url: $UNIDESK_PAC_REPOSITORY_URL
|
|
git_provider:
|
|
type: gitea
|
|
url: $UNIDESK_PAC_GITEA_BASE_URL
|
|
secret:
|
|
name: $UNIDESK_PAC_SECRET_NAME
|
|
key: $UNIDESK_PAC_TOKEN_KEY
|
|
webhook_secret:
|
|
name: $UNIDESK_PAC_SECRET_NAME
|
|
key: $UNIDESK_PAC_WEBHOOK_SECRET_KEY
|
|
concurrency_limit: $UNIDESK_PAC_CONCURRENCY_LIMIT
|
|
params:
|
|
$(printf '%s' "$UNIDESK_PAC_PARAMS_JSON" | node -e 'const fs=require("fs"); const p=JSON.parse(fs.readFileSync(0,"utf8")); for (const [name,value] of Object.entries(p)) console.log(` - name: ${name}\n value: ${JSON.stringify(String(value))}`);')
|
|
EOF
|
|
}
|
|
|
|
consumer_rbac_manifest() {
|
|
cat <<EOF
|
|
apiVersion: rbac.authorization.k8s.io/v1
|
|
kind: Role
|
|
metadata:
|
|
name: unidesk-pac-consumer-runner
|
|
namespace: $UNIDESK_PAC_TARGET_NAMESPACE
|
|
labels:
|
|
app.kubernetes.io/managed-by: unidesk
|
|
app.kubernetes.io/part-of: $UNIDESK_PAC_PART_OF
|
|
unidesk.ai/spec: $UNIDESK_PAC_SPEC
|
|
rules:
|
|
- apiGroups: ["tekton.dev"]
|
|
resources: ["pipelineruns", "taskruns"]
|
|
verbs: ["get", "list", "watch", "create", "patch", "update"]
|
|
- apiGroups: [""]
|
|
resources: ["pods", "pods/log"]
|
|
verbs: ["get", "list", "watch"]
|
|
---
|
|
apiVersion: rbac.authorization.k8s.io/v1
|
|
kind: RoleBinding
|
|
metadata:
|
|
name: unidesk-pac-consumer-runner-default
|
|
namespace: $UNIDESK_PAC_TARGET_NAMESPACE
|
|
labels:
|
|
app.kubernetes.io/managed-by: unidesk
|
|
app.kubernetes.io/part-of: $UNIDESK_PAC_PART_OF
|
|
unidesk.ai/spec: $UNIDESK_PAC_SPEC
|
|
subjects:
|
|
- kind: ServiceAccount
|
|
name: default
|
|
namespace: $UNIDESK_PAC_TARGET_NAMESPACE
|
|
roleRef:
|
|
apiGroup: rbac.authorization.k8s.io
|
|
kind: Role
|
|
name: unidesk-pac-consumer-runner
|
|
EOF
|
|
}
|
|
|
|
apply_release() {
|
|
tmp=$(mktemp)
|
|
printf '%s' "$UNIDESK_PAC_RELEASE_MANIFEST_B64" | base64 -d > "$tmp"
|
|
kubectl apply --server-side --force-conflicts --field-manager="$UNIDESK_PAC_FIELD_MANAGER" -f "$tmp" >/dev/null
|
|
rm -f "$tmp"
|
|
}
|
|
|
|
wait_ready() {
|
|
timeout="${UNIDESK_PAC_WAIT_TIMEOUT_SECONDS:-55}"
|
|
end=$(( $(now_epoch) + timeout ))
|
|
while [ "$(now_epoch)" -lt "$end" ]; do
|
|
ready=$(kubectl -n "$UNIDESK_PAC_RELEASE_NAMESPACE" get deploy "$UNIDESK_PAC_CONTROLLER_SERVICE_NAME" -o jsonpath='{.status.readyReplicas}' 2>/dev/null || true)
|
|
desired=$(kubectl -n "$UNIDESK_PAC_RELEASE_NAMESPACE" get deploy "$UNIDESK_PAC_CONTROLLER_SERVICE_NAME" -o jsonpath='{.spec.replicas}' 2>/dev/null || true)
|
|
if [ "${ready:-0}" = "${desired:-1}" ] && [ "${ready:-0}" != "0" ]; then
|
|
return 0
|
|
fi
|
|
sleep 2
|
|
done
|
|
return 1
|
|
}
|
|
|
|
apply_action() {
|
|
if [ "$UNIDESK_PAC_DRY_RUN" = "1" ]; then
|
|
printf '{"ok":true,"mode":"dry-run","mutation":false,"valuesPrinted":false}\n'
|
|
return
|
|
fi
|
|
apply_release
|
|
kubectl create ns "$UNIDESK_PAC_TARGET_NAMESPACE" --dry-run=client -o yaml | kubectl apply -f - >/dev/null
|
|
consumer_rbac_manifest | kubectl apply --server-side --force-conflicts --field-manager="$UNIDESK_PAC_FIELD_MANAGER" -f - >/dev/null
|
|
token=$(ensure_token)
|
|
test -n "$token"
|
|
kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" create secret generic "$UNIDESK_PAC_SECRET_NAME" \
|
|
--from-literal="$UNIDESK_PAC_TOKEN_KEY=$token" \
|
|
--from-literal="$UNIDESK_PAC_WEBHOOK_SECRET_KEY=$UNIDESK_PAC_WEBHOOK_SECRET" \
|
|
--dry-run=client -o yaml | kubectl apply -f - >/dev/null
|
|
repository_manifest | kubectl apply --server-side --force-conflicts --field-manager="$UNIDESK_PAC_FIELD_MANAGER" -f - >/dev/null
|
|
argo_bootstrap=false
|
|
if [ -n "${UNIDESK_PAC_ARGO_BOOTSTRAP_MANIFEST_B64:-}" ]; then
|
|
argo_tmp=$(mktemp)
|
|
printf '%s' "$UNIDESK_PAC_ARGO_BOOTSTRAP_MANIFEST_B64" | base64 -d >"$argo_tmp"
|
|
kubectl apply --server-side --force-conflicts --field-manager="$UNIDESK_PAC_FIELD_MANAGER" -f "$argo_tmp" >/dev/null
|
|
rm -f "$argo_tmp"
|
|
kubectl -n "$UNIDESK_PAC_ARGO_NAMESPACE" annotate application "$UNIDESK_PAC_ARGO_APPLICATION" argocd.argoproj.io/refresh=hard --overwrite >/dev/null
|
|
argo_bootstrap=true
|
|
fi
|
|
hook_id=$(ensure_webhook)
|
|
ready=false
|
|
if wait_ready; then ready=true; fi
|
|
printf '{"ok":%s,"mode":"confirmed","mutation":true,"controllerReady":%s,"argoBootstrap":%s,"webhook":{"present":true,"id":"%s","url":"%s"},"repository":"%s","namespace":"%s","valuesPrinted":false}\n' "$ready" "$ready" "$argo_bootstrap" "$(json_string "$hook_id")" "$(json_string "$UNIDESK_PAC_WEBHOOK_URL")" "$(json_string "$UNIDESK_PAC_REPOSITORY_NAME")" "$(json_string "$UNIDESK_PAC_TARGET_NAMESPACE")"
|
|
}
|
|
|
|
condition_status() {
|
|
kubectl -n "$1" get "$2" "$3" -o jsonpath='{range .status.conditions[*]}{.type}={.status}:{.reason}{";"}{end}' 2>/dev/null || true
|
|
}
|
|
|
|
pipeline_rows() {
|
|
payload_file=$(mktemp)
|
|
kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get pipelinerun -o json >"$payload_file" 2>/dev/null || printf '{"items":[]}' >"$payload_file"
|
|
node - "$payload_file" <<'NODE'
|
|
const fs = require('node:fs');
|
|
const input = fs.readFileSync(process.argv[2], 'utf8') || '{"items":[]}';
|
|
const data = input ? JSON.parse(input) : { items: [] };
|
|
function firstString(...values) {
|
|
for (const value of values) {
|
|
if (typeof value === 'string' && value.length > 0) return value;
|
|
}
|
|
return null;
|
|
}
|
|
function cond(item) {
|
|
const c = (item.status?.conditions || []).find((x) => x.type === 'Succeeded') || {};
|
|
return { status: c.status || '', reason: c.reason || '' };
|
|
}
|
|
function mapParams(params) {
|
|
const out = {};
|
|
for (const item of params || []) {
|
|
if (!item || typeof item.name !== 'string') continue;
|
|
if (typeof item.value === 'string') out[item.name] = item.value;
|
|
else if (item.value !== undefined && item.value !== null) out[item.name] = JSON.stringify(item.value);
|
|
}
|
|
return out;
|
|
}
|
|
function extractCommit(item, params) {
|
|
const labels = item.metadata?.labels || {};
|
|
const annotations = item.metadata?.annotations || {};
|
|
return firstString(
|
|
labels['pipelinesascode.tekton.dev/sha'],
|
|
labels['pipelinesascode.tekton.dev/commit'],
|
|
labels['agentrun.pikastech.local/source-commit'],
|
|
labels['unidesk.ai/source-commit'],
|
|
labels['hwlab.pikastech.local/source-commit'],
|
|
annotations['pipelinesascode.tekton.dev/sha'],
|
|
annotations['pipelinesascode.tekton.dev/commit'],
|
|
annotations['unidesk.ai/source-commit'],
|
|
params.revision,
|
|
params.source_revision,
|
|
params.sourceCommit,
|
|
params.source_commit,
|
|
params.git_sha,
|
|
params.sha,
|
|
params.commit
|
|
);
|
|
}
|
|
function durationSeconds(item) {
|
|
const start = item.status?.startTime;
|
|
const done = item.status?.completionTime;
|
|
if (!start) return null;
|
|
return Math.max(0, Math.round(((done ? Date.parse(done) : Date.now()) - Date.parse(start)) / 1000));
|
|
}
|
|
const prefix = process.env.UNIDESK_PAC_PIPELINE_RUN_PREFIX;
|
|
const pipeline = process.env.UNIDESK_PAC_PIPELINE_NAME;
|
|
const rows = (data.items || [])
|
|
.filter((item) => {
|
|
const labels = item.metadata?.labels || {};
|
|
const name = item.metadata?.name || '';
|
|
return name.startsWith(prefix)
|
|
|| labels['tekton.dev/pipeline'] === pipeline
|
|
|| labels['tekton.dev/pipelineName'] === pipeline;
|
|
})
|
|
.sort((a, b) => Date.parse(b.metadata?.creationTimestamp || 0) - Date.parse(a.metadata?.creationTimestamp || 0))
|
|
.slice(0, 8)
|
|
.map((item) => {
|
|
const c = cond(item);
|
|
const params = mapParams(item.spec?.params);
|
|
return {
|
|
name: item.metadata.name,
|
|
status: c.status,
|
|
reason: c.reason,
|
|
createdAt: item.metadata.creationTimestamp || null,
|
|
startTime: item.status?.startTime || null,
|
|
completionTime: item.status?.completionTime || null,
|
|
durationSeconds: durationSeconds(item),
|
|
sourceCommit: extractCommit(item, params),
|
|
};
|
|
});
|
|
process.stdout.write(JSON.stringify(rows));
|
|
NODE
|
|
rm -f "$payload_file"
|
|
}
|
|
|
|
history_rows() {
|
|
node <<'NODE'
|
|
const fs = require('node:fs');
|
|
const cp = require('node:child_process');
|
|
const { extractPacArtifactEvidence, parsePacLogRecords, taskTerminalRecord } = require(process.env.UNIDESK_PAC_EVALUATOR_PATH);
|
|
const limit = Math.max(1, Math.min(50, Number.parseInt(process.env.UNIDESK_PAC_HISTORY_LIMIT || '5', 10) || 5));
|
|
const detailId = process.env.UNIDESK_PAC_HISTORY_ID || '';
|
|
const consumers = JSON.parse(Buffer.from(process.env.UNIDESK_PAC_HISTORY_CONSUMERS_B64 || 'W10=', 'base64').toString('utf8'));
|
|
const displayTimeZone = process.env.UNIDESK_PAC_DISPLAY_TIME_ZONE || 'UTC';
|
|
const kubectlJsonErrors = [];
|
|
const displayTimeFormatter = new Intl.DateTimeFormat('sv-SE', {
|
|
timeZone: displayTimeZone,
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit',
|
|
hour12: false,
|
|
});
|
|
|
|
function kubectlJson(args, fallback, context) {
|
|
const tmpDir = fs.mkdtempSync('/tmp/unidesk-pac-history-');
|
|
const outPath = `${tmpDir}/kubectl.json`;
|
|
let outFd = null;
|
|
try {
|
|
outFd = fs.openSync(outPath, 'w');
|
|
const result = cp.spawnSync('kubectl', args, {
|
|
stdio: ['ignore', outFd, 'pipe'],
|
|
encoding: 'utf8',
|
|
timeout: 30000,
|
|
maxBuffer: 1024 * 1024,
|
|
});
|
|
fs.closeSync(outFd);
|
|
outFd = null;
|
|
if (result.error || result.status !== 0) {
|
|
kubectlJsonErrors.push({
|
|
context: context || args.join(' '),
|
|
status: result.status,
|
|
error: result.error ? String(result.error.message || result.error) : null,
|
|
stderr: String(result.stderr || '').slice(0, 1000),
|
|
});
|
|
return fallback;
|
|
}
|
|
const out = fs.readFileSync(outPath, 'utf8');
|
|
return JSON.parse(out || JSON.stringify(fallback));
|
|
} catch (error) {
|
|
kubectlJsonErrors.push({
|
|
context: context || args.join(' '),
|
|
status: null,
|
|
error: String(error && error.message ? error.message : error).slice(0, 1000),
|
|
stderr: null,
|
|
});
|
|
return fallback;
|
|
} finally {
|
|
if (outFd !== null) {
|
|
try { fs.closeSync(outFd); } catch {}
|
|
}
|
|
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
}
|
|
}
|
|
|
|
function condition(item) {
|
|
const c = (item.status?.conditions || []).find((x) => x.type === 'Succeeded') || {};
|
|
return { status: c.status || '', reason: c.reason || '', message: c.message || '' };
|
|
}
|
|
|
|
function durationSeconds(start, done) {
|
|
if (!start) return null;
|
|
const startMs = Date.parse(start);
|
|
const doneMs = done ? Date.parse(done) : Date.now();
|
|
if (!Number.isFinite(startMs) || !Number.isFinite(doneMs)) return null;
|
|
return Math.max(0, Math.round((doneMs - startMs) / 1000));
|
|
}
|
|
|
|
function formatDisplayTime(value) {
|
|
if (!value) return null;
|
|
const date = new Date(value);
|
|
if (!Number.isFinite(date.getTime())) return null;
|
|
return displayTimeFormatter.format(date);
|
|
}
|
|
|
|
function mapParams(params) {
|
|
const out = {};
|
|
for (const item of params || []) {
|
|
if (!item || typeof item.name !== 'string') continue;
|
|
if (typeof item.value === 'string') out[item.name] = item.value;
|
|
else if (item.value !== undefined && item.value !== null) out[item.name] = JSON.stringify(item.value);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function firstString(...values) {
|
|
for (const value of values) {
|
|
if (typeof value === 'string' && value.length > 0) return value;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function extractCommit(item, params) {
|
|
const labels = item.metadata?.labels || {};
|
|
const annotations = item.metadata?.annotations || {};
|
|
return firstString(
|
|
labels['pipelinesascode.tekton.dev/sha'],
|
|
labels['pipelinesascode.tekton.dev/commit'],
|
|
labels['unidesk.ai/source-commit'],
|
|
labels['hwlab.pikastech.local/source-commit'],
|
|
labels['agentrun.pikastech.local/source-commit'],
|
|
annotations['pipelinesascode.tekton.dev/sha'],
|
|
annotations['pipelinesascode.tekton.dev/commit'],
|
|
annotations['unidesk.ai/source-commit'],
|
|
params.revision,
|
|
params.source_revision,
|
|
params.sourceCommit,
|
|
params.source_commit,
|
|
params.git_sha,
|
|
params.sha,
|
|
params.commit
|
|
);
|
|
}
|
|
|
|
function extractBranch(item, params) {
|
|
const labels = item.metadata?.labels || {};
|
|
const annotations = item.metadata?.annotations || {};
|
|
return firstString(
|
|
labels['pipelinesascode.tekton.dev/branch'],
|
|
annotations['pipelinesascode.tekton.dev/branch'],
|
|
params.source_branch,
|
|
params.branch,
|
|
params.git_branch
|
|
);
|
|
}
|
|
|
|
function recursiveEnvReuse(value, state) {
|
|
if (value === null || value === undefined) return;
|
|
if (Array.isArray(value)) {
|
|
for (const item of value) recursiveEnvReuse(item, state);
|
|
return;
|
|
}
|
|
if (typeof value !== 'object') return;
|
|
if (typeof value.buildSkippedCount === 'number') state.buildSkippedCount = value.buildSkippedCount;
|
|
if (typeof value.serviceReusedCount === 'number') state.serviceReusedCount = value.serviceReusedCount;
|
|
if (typeof value.reusedServiceCount === 'number') state.serviceReusedCount = value.reusedServiceCount;
|
|
if (typeof value.envReuse === 'string') state.status = value.envReuse;
|
|
if (typeof value.envReuseStatus === 'string') state.status = value.envReuseStatus;
|
|
if (value.envReuse && typeof value.envReuse === 'object') {
|
|
const env = value.envReuse;
|
|
if (typeof env.mode === 'string') state.status = env.mode;
|
|
if (typeof env.status === 'string') state.status = env.status;
|
|
if (typeof env.dependencyReuse === 'string') state.status = env.dependencyReuse;
|
|
if (typeof env.buildSkippedCount === 'number') state.buildSkippedCount = env.buildSkippedCount;
|
|
if (typeof env.serviceReusedCount === 'number') state.serviceReusedCount = env.serviceReusedCount;
|
|
}
|
|
if (typeof value.status === 'string' && /reus|skip|built|publish/u.test(value.status)) state.status = value.status;
|
|
for (const item of Object.values(value)) recursiveEnvReuse(item, state);
|
|
}
|
|
|
|
function envReuseForPipelineRun(namespace, name, taskRuns) {
|
|
const state = { status: null, buildSkippedCount: null, serviceReusedCount: null, buildCache: null, source: 'pipeline-task-logs', sourceObservation: null, artifact: null, collector: null };
|
|
const matching = (taskRuns.items || []).filter((item) => item.metadata?.labels?.['tekton.dev/pipelineRun'] === name);
|
|
const contractTaskRuns = matching.filter((item) => taskTerminalRecord(item) !== null);
|
|
const terminalRecords = contractTaskRuns.map(taskTerminalRecord).filter(Boolean);
|
|
const logErrors = [];
|
|
let logs = '';
|
|
for (const item of contractTaskRuns) {
|
|
const podName = item.status?.podName;
|
|
if (!podName) {
|
|
logErrors.push({ taskRun: item.metadata?.name || null, reason: 'pod-name-missing' });
|
|
continue;
|
|
}
|
|
const result = cp.spawnSync('kubectl', ['-n', namespace, 'logs', podName, '--all-containers=true'], {
|
|
encoding: 'utf8',
|
|
timeout: 12000,
|
|
maxBuffer: 4 * 1024 * 1024,
|
|
});
|
|
if (result.error || result.status !== 0) {
|
|
logErrors.push({ taskRun: item.metadata?.name || null, reason: result.error ? String(result.error.message || result.error) : String(result.stderr || '').slice(0, 240) });
|
|
continue;
|
|
}
|
|
logs += `${result.stdout || ''}\n`;
|
|
}
|
|
if (contractTaskRuns.length === 0) {
|
|
state.source = 'pipeline-run-log-fallback';
|
|
const result = cp.spawnSync('kubectl', ['-n', namespace, 'logs', '-l', `tekton.dev/pipelineRun=${name}`, '--all-containers=true', '--tail=320'], {
|
|
encoding: 'utf8',
|
|
timeout: 12000,
|
|
maxBuffer: 2 * 1024 * 1024,
|
|
});
|
|
if (result.error || result.status !== 0) {
|
|
return { ...state, source: 'logs-unavailable', collector: { mode: state.source, contractTaskCount: 0, terminalRecordCount: 0, logErrorCount: 1, valuesPrinted: false } };
|
|
}
|
|
logs = result.stdout || '';
|
|
}
|
|
const lines = logs.split(/\r?\n/u);
|
|
const headerIndex = lines.findIndex((line) => /^ENV_REUSE\s+NODE_DEPS\s+/u.test(line.trim()));
|
|
if (headerIndex >= 0) {
|
|
const row = (lines.slice(headerIndex + 1).find((line) => line.trim()) || '').trim().split(/\s+/u);
|
|
if (row.length >= 5) {
|
|
state.status = row[0];
|
|
state.buildCache = row[4];
|
|
}
|
|
}
|
|
const records = [...parsePacLogRecords(logs), ...terminalRecords];
|
|
for (const parsed of records) recursiveEnvReuse(parsed, state);
|
|
state.artifact = extractPacArtifactEvidence(records, logs);
|
|
state.sourceObservation = state.artifact.sourceObservation;
|
|
state.collector = {
|
|
mode: state.source,
|
|
contractTaskCount: contractTaskRuns.length,
|
|
terminalRecordCount: terminalRecords.length,
|
|
logErrorCount: logErrors.length,
|
|
logErrors,
|
|
valuesPrinted: false,
|
|
};
|
|
return state;
|
|
}
|
|
|
|
function failedStep(item) {
|
|
const steps = (item.status?.steps || []).map((step) => ({
|
|
name: step.name || null,
|
|
container: step.container || null,
|
|
exitCode: Number.isInteger(step.terminated?.exitCode) ? step.terminated.exitCode : null,
|
|
reason: step.terminated?.reason || null,
|
|
terminationReason: step.terminationReason || null,
|
|
}));
|
|
return steps.find((step) => step.exitCode !== null && step.exitCode !== 0 && step.terminationReason !== 'Skipped')
|
|
|| steps.find((step) => step.terminationReason === 'Error')
|
|
|| null;
|
|
}
|
|
|
|
function taskSummary(taskRuns, name, namespace) {
|
|
const rows = (taskRuns.items || [])
|
|
.filter((item) => item.metadata?.labels?.['tekton.dev/pipelineRun'] === name)
|
|
.map((item) => {
|
|
const c = condition(item);
|
|
return {
|
|
name: item.metadata?.name || '',
|
|
status: c.status || null,
|
|
reason: c.reason || null,
|
|
message: c.message || null,
|
|
failedStep: failedStep(item),
|
|
startTime: item.status?.startTime || null,
|
|
completionTime: item.status?.completionTime || null,
|
|
durationSeconds: durationSeconds(item.status?.startTime, item.status?.completionTime),
|
|
};
|
|
});
|
|
const longest = [...rows].sort((a, b) => (b.durationSeconds || 0) - (a.durationSeconds || 0))[0] || null;
|
|
const failed = rows.find((row) => row.status === 'False') || null;
|
|
const target = process.env.UNIDESK_PAC_TARGET_ID || '';
|
|
return {
|
|
count: rows.length,
|
|
longest,
|
|
failed,
|
|
details: detailId ? rows : undefined,
|
|
logsCommand: target && namespace
|
|
? `trans ${target}:k3s logs -n ${namespace} -l tekton.dev/pipelineRun=${name} --all-containers --tail=240`
|
|
: null,
|
|
};
|
|
}
|
|
|
|
function rowFor(consumer, item, taskRuns) {
|
|
const params = mapParams(item.spec?.params);
|
|
const c = condition(item);
|
|
const tasks = taskSummary(taskRuns, item.metadata.name, consumer.namespace);
|
|
const labels = item.metadata?.labels || {};
|
|
const annotations = item.metadata?.annotations || {};
|
|
const sourceCommit = extractCommit(item, params);
|
|
const evidence = envReuseForPipelineRun(consumer.namespace, item.metadata.name, taskRuns);
|
|
const sourceObservation = evidence.sourceObservation === null ? null : {
|
|
...evidence.sourceObservation,
|
|
sourceMatched: sourceCommit !== null && evidence.sourceObservation.sourceCommit === sourceCommit,
|
|
};
|
|
return {
|
|
id: item.metadata.name,
|
|
consumer: consumer.id,
|
|
repository: consumer.repository,
|
|
repo: consumer.repo,
|
|
repoUrl: consumer.repoUrl,
|
|
pipeline: consumer.pipeline,
|
|
pipelineRun: item.metadata.name,
|
|
triggeredAt: item.metadata?.creationTimestamp || item.status?.startTime || null,
|
|
startTime: item.status?.startTime || null,
|
|
completionTime: item.status?.completionTime || null,
|
|
displayTime: formatDisplayTime(item.metadata?.creationTimestamp || item.status?.startTime || null),
|
|
displayStartTime: formatDisplayTime(item.status?.startTime || null),
|
|
displayCompletionTime: formatDisplayTime(item.status?.completionTime || null),
|
|
displayTimeZone,
|
|
durationSeconds: durationSeconds(item.status?.startTime || item.metadata?.creationTimestamp, item.status?.completionTime),
|
|
status: c.status || null,
|
|
reason: c.reason || null,
|
|
commit: sourceCommit,
|
|
branch: extractBranch(item, params),
|
|
eventType: firstString(labels['pipelinesascode.tekton.dev/event-type'], annotations['pipelinesascode.tekton.dev/event-type']),
|
|
sender: firstString(labels['pipelinesascode.tekton.dev/sender'], annotations['pipelinesascode.tekton.dev/sender']),
|
|
envReuse: {
|
|
status: evidence.status,
|
|
buildSkippedCount: evidence.buildSkippedCount,
|
|
serviceReusedCount: evidence.serviceReusedCount,
|
|
buildCache: evidence.buildCache,
|
|
source: evidence.source,
|
|
},
|
|
sourceObservation,
|
|
artifact: evidence.artifact,
|
|
collector: evidence.collector,
|
|
taskRuns: tasks,
|
|
source: 'gitea-pac-tekton-live',
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
function pipelineRunMatchesConsumer(consumer, item) {
|
|
const labels = item.metadata?.labels || {};
|
|
const name = item.metadata?.name || '';
|
|
return name.startsWith(consumer.pipelineRunPrefix)
|
|
|| labels['tekton.dev/pipeline'] === consumer.pipeline
|
|
|| labels['tekton.dev/pipelineName'] === consumer.pipeline;
|
|
}
|
|
|
|
const rows = [];
|
|
const consumerSummaries = [];
|
|
for (const consumer of consumers) {
|
|
const pipelineRuns = kubectlJson(['-n', consumer.namespace, 'get', 'pipelinerun', '-o', 'json'], { items: [] }, `${consumer.id}:pipelinerun`);
|
|
const taskRuns = kubectlJson(['-n', consumer.namespace, 'get', 'taskrun', '-o', 'json'], { items: [] }, `${consumer.id}:taskrun`);
|
|
let matches = (pipelineRuns.items || []).filter((item) => {
|
|
const name = item.metadata?.name || '';
|
|
if (detailId) return name === detailId && pipelineRunMatchesConsumer(consumer, item);
|
|
return pipelineRunMatchesConsumer(consumer, item);
|
|
});
|
|
matches = matches.sort((a, b) => Date.parse(b.metadata?.creationTimestamp || 0) - Date.parse(a.metadata?.creationTimestamp || 0));
|
|
if (!detailId) matches = matches.slice(0, limit);
|
|
for (const item of matches) rows.push(rowFor(consumer, item, taskRuns));
|
|
consumerSummaries.push({ id: consumer.id, namespace: consumer.namespace, repository: consumer.repository, pipelineRunPrefix: consumer.pipelineRunPrefix, totalPipelineRuns: (pipelineRuns.items || []).length, matched: matches.length });
|
|
}
|
|
rows.sort((a, b) => Date.parse(b.triggeredAt || 0) - Date.parse(a.triggeredAt || 0));
|
|
process.stdout.write(JSON.stringify({ rows, consumers: consumerSummaries, errors: kubectlJsonErrors }));
|
|
NODE
|
|
}
|
|
|
|
task_rows() {
|
|
payload_file=$(mktemp)
|
|
kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get taskrun -o json >"$payload_file" 2>/dev/null || printf '{"items":[]}' >"$payload_file"
|
|
node - "$payload_file" <<'NODE'
|
|
const fs = require('node:fs');
|
|
const input = fs.readFileSync(process.argv[2], 'utf8') || '{"items":[]}';
|
|
const data = input ? JSON.parse(input) : { items: [] };
|
|
const pr = process.env.UNIDESK_PAC_TARGET_PIPELINERUN || '';
|
|
if (!pr) {
|
|
process.stdout.write('[]');
|
|
process.exit(0);
|
|
}
|
|
function cond(item) {
|
|
const c = (item.status?.conditions || []).find((x) => x.type === 'Succeeded') || {};
|
|
return { status: c.status || '', reason: c.reason || '', message: c.message || '' };
|
|
}
|
|
function failedStep(item) {
|
|
const steps = (item.status?.steps || []).map((step) => ({
|
|
name: step.name || null,
|
|
exitCode: Number.isInteger(step.terminated?.exitCode) ? step.terminated.exitCode : null,
|
|
reason: step.terminated?.reason || null,
|
|
terminationReason: step.terminationReason || null,
|
|
}));
|
|
return steps.find((step) => step.exitCode !== null && step.exitCode !== 0 && step.terminationReason !== 'Skipped')
|
|
|| steps.find((step) => step.terminationReason === 'Error')
|
|
|| null;
|
|
}
|
|
function durationSeconds(item) {
|
|
const start = item.status?.startTime;
|
|
const done = item.status?.completionTime;
|
|
if (!start) return null;
|
|
return Math.max(0, Math.round(((done ? Date.parse(done) : Date.now()) - Date.parse(start)) / 1000));
|
|
}
|
|
const rows = (data.items || [])
|
|
.filter((item) => item.metadata?.labels?.['tekton.dev/pipelineRun'] === pr)
|
|
.sort((a, b) => Date.parse(a.metadata?.creationTimestamp || 0) - Date.parse(b.metadata?.creationTimestamp || 0))
|
|
.slice(0, 20)
|
|
.map((item) => {
|
|
const c = cond(item);
|
|
return { name: item.metadata.name, pipelineRun: item.metadata.labels?.['tekton.dev/pipelineRun'] || null, status: c.status, reason: c.reason, message: c.message || null, failedStep: failedStep(item), startTime: item.status?.startTime || null, completionTime: item.status?.completionTime || null, durationSeconds: durationSeconds(item) };
|
|
});
|
|
process.stdout.write(JSON.stringify(rows));
|
|
NODE
|
|
rm -f "$payload_file"
|
|
}
|
|
|
|
artifact_summary() {
|
|
if [ -z "${UNIDESK_PAC_TARGET_PIPELINERUN:-}" ]; then
|
|
printf '{}'
|
|
return
|
|
fi
|
|
task_file=$(mktemp)
|
|
kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get taskrun -l "tekton.dev/pipelineRun=$UNIDESK_PAC_TARGET_PIPELINERUN" -o json >"$task_file" 2>/dev/null || printf '{"items":[]}' >"$task_file"
|
|
node - "$task_file" <<'NODE'
|
|
const fs = require('node:fs');
|
|
const cp = require('node:child_process');
|
|
const { extractPacArtifactEvidence, parsePacLogRecords, taskTerminalRecord } = require(process.env.UNIDESK_PAC_EVALUATOR_PATH);
|
|
const input = JSON.parse(fs.readFileSync(process.argv[2], 'utf8') || '{"items":[]}');
|
|
const namespace = process.env.UNIDESK_PAC_TARGET_NAMESPACE;
|
|
const pipelineRun = process.env.UNIDESK_PAC_TARGET_PIPELINERUN;
|
|
const taskRuns = (input.items || []).filter((item) => item.metadata?.labels?.['tekton.dev/pipelineRun'] === pipelineRun);
|
|
const contractTaskRuns = taskRuns.filter((item) => taskTerminalRecord(item) !== null);
|
|
const terminalRecords = contractTaskRuns.map(taskTerminalRecord).filter(Boolean);
|
|
const logErrors = [];
|
|
let logText = '';
|
|
for (const item of contractTaskRuns) {
|
|
const podName = item.status?.podName;
|
|
if (!podName) {
|
|
logErrors.push({ taskRun: item.metadata?.name || null, reason: 'pod-name-missing' });
|
|
continue;
|
|
}
|
|
const result = cp.spawnSync('kubectl', ['-n', namespace, 'logs', podName, '--all-containers=true'], {
|
|
encoding: 'utf8',
|
|
timeout: 12000,
|
|
maxBuffer: 4 * 1024 * 1024,
|
|
});
|
|
if (result.error || result.status !== 0) {
|
|
logErrors.push({ taskRun: item.metadata?.name || null, reason: result.error ? String(result.error.message || result.error) : String(result.stderr || '').slice(0, 240) });
|
|
continue;
|
|
}
|
|
logText += `${result.stdout || ''}\n`;
|
|
}
|
|
let mode = 'pipeline-task-logs';
|
|
if (contractTaskRuns.length === 0) {
|
|
mode = 'pipeline-run-log-fallback';
|
|
const fallback = cp.spawnSync('kubectl', ['-n', namespace, 'logs', '-l', `tekton.dev/pipelineRun=${pipelineRun}`, '--all-containers=true', '--tail=320'], {
|
|
encoding: 'utf8',
|
|
timeout: 12000,
|
|
maxBuffer: 2 * 1024 * 1024,
|
|
});
|
|
if (!fallback.error && fallback.status === 0) logText = fallback.stdout || '';
|
|
else logErrors.push({ taskRun: null, reason: fallback.error ? String(fallback.error.message || fallback.error) : String(fallback.stderr || '').slice(0, 240) });
|
|
}
|
|
const records = [...parsePacLogRecords(logText), ...terminalRecords];
|
|
const out = extractPacArtifactEvidence(records, logText);
|
|
out.collector = {
|
|
mode,
|
|
contractTaskCount: contractTaskRuns.length,
|
|
terminalRecordCount: terminalRecords.length,
|
|
logErrorCount: logErrors.length,
|
|
logErrors,
|
|
valuesPrinted: false,
|
|
};
|
|
process.stdout.write(JSON.stringify(out));
|
|
NODE
|
|
rm -f "$task_file"
|
|
}
|
|
|
|
runtime_summary() {
|
|
app_file=$(mktemp)
|
|
params_file=$(mktemp)
|
|
kubectl -n "$UNIDESK_PAC_ARGO_NAMESPACE" get application "$UNIDESK_PAC_ARGO_APPLICATION" -o json >"$app_file" 2>/dev/null || printf '{}' >"$app_file"
|
|
printf '%s' "$UNIDESK_PAC_PARAMS_JSON" >"$params_file"
|
|
target=$(node - "$app_file" "$params_file" <<'NODE'
|
|
const fs = require('node:fs');
|
|
const app = JSON.parse(fs.readFileSync(process.argv[2], 'utf8') || '{}');
|
|
const params = JSON.parse(fs.readFileSync(process.argv[3], 'utf8') || '{}');
|
|
const prefix = process.env.UNIDESK_PAC_CONSUMER_ID === 'unidesk-host' ? 'unidesk_host_' : '';
|
|
const param = (name) => params[`${prefix}${name}`] ?? params[name];
|
|
if (param('runtime_namespace') && param('runtime_deployment')) {
|
|
process.stdout.write(`${param('runtime_namespace')}\t${param('runtime_deployment')}`);
|
|
process.exit(0);
|
|
}
|
|
const resource = (app.status?.resources || []).find((item) => item.kind === 'Deployment' && item.namespace && item.name);
|
|
if (resource) process.stdout.write(`${resource.namespace}\t${resource.name}`);
|
|
NODE
|
|
)
|
|
rm -f "$app_file" "$params_file"
|
|
if [ -z "$target" ]; then
|
|
printf '{}'
|
|
return
|
|
fi
|
|
namespace=$(printf '%s' "$target" | cut -f1)
|
|
name=$(printf '%s' "$target" | cut -f2)
|
|
deploy_file=$(mktemp)
|
|
kubectl -n "$namespace" get deploy "$name" -o json >"$deploy_file" 2>/dev/null || printf '{}' >"$deploy_file"
|
|
node - "$deploy_file" <<'NODE' || printf '{}'
|
|
const fs = require('node:fs');
|
|
const input = fs.readFileSync(process.argv[2], 'utf8').trim();
|
|
if (!input) {
|
|
process.stdout.write('{}');
|
|
process.exit(0);
|
|
}
|
|
const deploy = JSON.parse(input);
|
|
const image = deploy.spec?.template?.spec?.containers?.[0]?.image || null;
|
|
const digest = image && String(image).includes('@') ? String(image).split('@').slice(1).join('@') : null;
|
|
process.stdout.write(JSON.stringify({
|
|
namespace: deploy.metadata?.namespace || null,
|
|
deployment: deploy.metadata?.name || null,
|
|
readyReplicas: deploy.status?.readyReplicas ?? null,
|
|
replicas: deploy.spec?.replicas ?? null,
|
|
image,
|
|
digest,
|
|
valuesPrinted: false,
|
|
}));
|
|
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);
|
|
line('retained', artifact.sourceObservation?.mode === 'no-runtime-change' && artifact.sourceObservation?.valid === true ? 'true' : 'false');
|
|
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)
|
|
retained=$(printf '%s\n' "$fields" | sed -n 's/^retained=//p' | base64 -d 2>/dev/null || true)
|
|
relation=unknown
|
|
proof=unavailable
|
|
reason=missing-revision-context
|
|
fetch_ok=false
|
|
if [ "$retained" = true ] && printf '%s' "$observed" | grep -Eq '^([0-9a-fA-F]{40}|[0-9a-fA-F]{64})$'; then
|
|
relation=retained
|
|
proof=structured-no-runtime-change-plan
|
|
reason=source-observation-retains-current-revision
|
|
elif 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)
|
|
artifact_file=$(mktemp)
|
|
argo_file=$(mktemp)
|
|
runtime_file=$(mktemp)
|
|
printf '%s' "$UNIDESK_PAC_PARAMS_JSON" >"$params_file"
|
|
printf '%s' "${1:-[]}" >"$pipelines_file"
|
|
printf '%s' "${2:-{}}" >"$artifact_file"
|
|
printf '%s' "${3:-{}}" >"$argo_file"
|
|
printf '%s' "${4:-{}}" >"$runtime_file"
|
|
probe_env=$(node - "$params_file" "$pipelines_file" "$artifact_file" <<'NODE'
|
|
const fs = require('node:fs');
|
|
function parseLoose(path, fallback) {
|
|
const input = fs.readFileSync(path, 'utf8').trim();
|
|
if (!input) return fallback;
|
|
for (let end = input.length; end > 0; end -= 1) {
|
|
try { return JSON.parse(input.slice(0, end)); } catch {}
|
|
}
|
|
return fallback;
|
|
}
|
|
const params = parseLoose(process.argv[2], {});
|
|
const pipelines = parseLoose(process.argv[3], []);
|
|
const artifact = parseLoose(process.argv[4], {});
|
|
const latest = Array.isArray(pipelines) ? pipelines[0] || {} : {};
|
|
const sourceCommit = latest.sourceCommit || null;
|
|
const noRuntimeChange = artifact.sourceObservation?.mode === 'no-runtime-change' && artifact.sourceObservation?.valid === true;
|
|
const runtimeSourceCommit = artifact.runtimeSourceCommit || (noRuntimeChange ? null : sourceCommit);
|
|
const tag = !noRuntimeChange && runtimeSourceCommit ? String(runtimeSourceCommit).slice(0, 12) : '';
|
|
const prefix = process.env.UNIDESK_PAC_CONSUMER_ID === 'unidesk-host' ? 'unidesk_host_' : '';
|
|
const param = (name) => params[`${prefix}${name}`] ?? params[name];
|
|
const imageRepository = typeof param('image_repository') === 'string' ? param('image_repository') : '';
|
|
const probeBase = typeof param('registry_probe_base') === 'string' ? param('registry_probe_base').replace(/\/+$/u, '') : '';
|
|
let registryUrl = '';
|
|
if (imageRepository && tag) {
|
|
const firstSlash = imageRepository.indexOf('/');
|
|
const registry = firstSlash >= 0 ? imageRepository.slice(0, firstSlash) : imageRepository;
|
|
const path = firstSlash >= 0 ? imageRepository.slice(firstSlash + 1) : '';
|
|
const base = probeBase || (registry ? `http://${registry}` : '');
|
|
registryUrl = base && path ? `${base}/v2/${path}/manifests/${tag}` : '';
|
|
}
|
|
function line(key, value) {
|
|
process.stdout.write(`${key}=${Buffer.from(String(value || ''), 'utf8').toString('base64')}\n`);
|
|
}
|
|
line('imageRepository', imageRepository);
|
|
line('registryProbeBase', probeBase);
|
|
line('sourceCommit', sourceCommit || '');
|
|
line('runtimeSourceCommit', runtimeSourceCommit || '');
|
|
line('tag', tag);
|
|
line('registryUrl', registryUrl);
|
|
line('sentinelId', param('sentinel_id') || '');
|
|
line('gitopsBranch', param('gitops_branch') || '');
|
|
line('gitopsManifestPath', param('gitops_manifest_path') || '');
|
|
line('runtimeNamespace', param('runtime_namespace') || '');
|
|
line('runtimeService', param('runtime_service') || '');
|
|
line('runtimeServicePort', param('runtime_service_port') || '');
|
|
line('healthPath', param('health_path') || '');
|
|
line('healthUrl', param('health_url') || '');
|
|
NODE
|
|
)
|
|
image_repository=$(printf '%s\n' "$probe_env" | sed -n 's/^imageRepository=//p' | base64 -d 2>/dev/null || true)
|
|
registry_probe_base=$(printf '%s\n' "$probe_env" | sed -n 's/^registryProbeBase=//p' | base64 -d 2>/dev/null || true)
|
|
source_commit=$(printf '%s\n' "$probe_env" | sed -n 's/^sourceCommit=//p' | base64 -d 2>/dev/null || true)
|
|
runtime_source_commit=$(printf '%s\n' "$probe_env" | sed -n 's/^runtimeSourceCommit=//p' | base64 -d 2>/dev/null || true)
|
|
image_tag=$(printf '%s\n' "$probe_env" | sed -n 's/^tag=//p' | base64 -d 2>/dev/null || true)
|
|
registry_url=$(printf '%s\n' "$probe_env" | sed -n 's/^registryUrl=//p' | base64 -d 2>/dev/null || true)
|
|
sentinel_id=$(printf '%s\n' "$probe_env" | sed -n 's/^sentinelId=//p' | base64 -d 2>/dev/null || true)
|
|
gitops_branch=$(printf '%s\n' "$probe_env" | sed -n 's/^gitopsBranch=//p' | base64 -d 2>/dev/null || true)
|
|
gitops_manifest_path=$(printf '%s\n' "$probe_env" | sed -n 's/^gitopsManifestPath=//p' | base64 -d 2>/dev/null || true)
|
|
runtime_namespace=$(printf '%s\n' "$probe_env" | sed -n 's/^runtimeNamespace=//p' | base64 -d 2>/dev/null || true)
|
|
runtime_service=$(printf '%s\n' "$probe_env" | sed -n 's/^runtimeService=//p' | base64 -d 2>/dev/null || true)
|
|
runtime_service_port=$(printf '%s\n' "$probe_env" | sed -n 's/^runtimeServicePort=//p' | base64 -d 2>/dev/null || true)
|
|
health_path=$(printf '%s\n' "$probe_env" | sed -n 's/^healthPath=//p' | base64 -d 2>/dev/null || true)
|
|
health_url=$(printf '%s\n' "$probe_env" | sed -n 's/^healthUrl=//p' | base64 -d 2>/dev/null || true)
|
|
registry_present=false
|
|
registry_digest=""
|
|
if [ -n "$registry_url" ]; then
|
|
header_file=$(mktemp)
|
|
if curl -fsS -D "$header_file" -o /dev/null -H 'Accept: application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.list.v2+json' "$registry_url" >/dev/null 2>&1; then
|
|
registry_present=true
|
|
fi
|
|
headers=$(cat "$header_file" 2>/dev/null || true)
|
|
rm -f "$header_file"
|
|
if printf '%s' "$headers" | grep -qi '^HTTP/.* 200'; then registry_present=true; fi
|
|
registry_digest=$(printf '%s\n' "$headers" | awk 'BEGIN{IGNORECASE=1} /^Docker-Content-Digest:/ { sub(/\r$/,"",$2); print $2; exit }')
|
|
fi
|
|
registry_present_json=false
|
|
if [ "$registry_present" = true ]; then registry_present_json=true; fi
|
|
health_status=""
|
|
if [ -n "$health_url" ]; then
|
|
if [ -n "$runtime_namespace" ] && [ -n "$runtime_service" ] && [ -n "$runtime_service_port" ] && [ -n "$health_path" ]; then
|
|
health_file=$(mktemp)
|
|
if kubectl get --raw "/api/v1/namespaces/$runtime_namespace/services/$runtime_service:$runtime_service_port/proxy$health_path" >"$health_file" 2>/dev/null \
|
|
&& node - "$health_file" <<'NODE'
|
|
const fs = require('node:fs');
|
|
const body = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
|
process.exit(body?.ok === true ? 0 : 1);
|
|
NODE
|
|
then
|
|
health_status=200
|
|
else
|
|
health_status=503
|
|
fi
|
|
rm -f "$health_file"
|
|
else
|
|
health_status=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 8 "$health_url" 2>/dev/null || true)
|
|
fi
|
|
fi
|
|
export UNIDESK_PAC_DIAG_IMAGE_REPOSITORY="$image_repository"
|
|
export UNIDESK_PAC_DIAG_REGISTRY_PROBE_BASE="$registry_probe_base"
|
|
export UNIDESK_PAC_DIAG_SOURCE_COMMIT="$source_commit"
|
|
export UNIDESK_PAC_DIAG_RUNTIME_SOURCE_COMMIT="$runtime_source_commit"
|
|
export UNIDESK_PAC_DIAG_IMAGE_TAG="$image_tag"
|
|
export UNIDESK_PAC_DIAG_REGISTRY_URL="$registry_url"
|
|
export UNIDESK_PAC_DIAG_REGISTRY_PRESENT="$registry_present_json"
|
|
export UNIDESK_PAC_DIAG_REGISTRY_DIGEST="$registry_digest"
|
|
export UNIDESK_PAC_DIAG_SENTINEL_ID="$sentinel_id"
|
|
export UNIDESK_PAC_DIAG_GITOPS_BRANCH="$gitops_branch"
|
|
export UNIDESK_PAC_DIAG_GITOPS_MANIFEST_PATH="$gitops_manifest_path"
|
|
export UNIDESK_PAC_DIAG_HEALTH_URL="$health_url"
|
|
export UNIDESK_PAC_DIAG_HEALTH_STATUS="$health_status"
|
|
node - "$params_file" "$pipelines_file" "$artifact_file" "$argo_file" "$runtime_file" <<'NODE'
|
|
const fs = require('node:fs');
|
|
const { evaluatePacStatus } = require(process.env.UNIDESK_PAC_EVALUATOR_PATH);
|
|
function parseLoose(path, fallback) {
|
|
const input = fs.readFileSync(path, 'utf8').trim();
|
|
if (!input) return fallback;
|
|
for (let end = input.length; end > 0; end -= 1) {
|
|
try { return JSON.parse(input.slice(0, end)); } catch {}
|
|
}
|
|
return fallback;
|
|
}
|
|
const pipelines = parseLoose(process.argv[3], []);
|
|
const artifact = parseLoose(process.argv[4], {});
|
|
const argo = parseLoose(process.argv[5], {});
|
|
const runtime = parseLoose(process.argv[6], {});
|
|
const latest = Array.isArray(pipelines) ? pipelines[0] || {} : {};
|
|
const evaluated = evaluatePacStatus({
|
|
sourceCommit: process.env.UNIDESK_PAC_DIAG_SOURCE_COMMIT || latest.sourceCommit || null,
|
|
runtimeSourceCommit: process.env.UNIDESK_PAC_DIAG_RUNTIME_SOURCE_COMMIT || artifact.runtimeSourceCommit || null,
|
|
imageRepository: process.env.UNIDESK_PAC_DIAG_IMAGE_REPOSITORY || null,
|
|
registryProbeBase: process.env.UNIDESK_PAC_DIAG_REGISTRY_PROBE_BASE || null,
|
|
imageTag: process.env.UNIDESK_PAC_DIAG_IMAGE_TAG || null,
|
|
registryUrl: process.env.UNIDESK_PAC_DIAG_REGISTRY_URL || null,
|
|
registryPresent: process.env.UNIDESK_PAC_DIAG_REGISTRY_PRESENT === 'true',
|
|
registryDigest: process.env.UNIDESK_PAC_DIAG_REGISTRY_DIGEST || null,
|
|
gitopsBranch: process.env.UNIDESK_PAC_DIAG_GITOPS_BRANCH || null,
|
|
gitopsManifestPath: process.env.UNIDESK_PAC_DIAG_GITOPS_MANIFEST_PATH || null,
|
|
healthUrl: process.env.UNIDESK_PAC_DIAG_HEALTH_URL || null,
|
|
healthStatus: process.env.UNIDESK_PAC_DIAG_HEALTH_STATUS || null,
|
|
pipelineRun: latest.name || null,
|
|
artifact,
|
|
argo,
|
|
runtime,
|
|
});
|
|
process.stdout.write(JSON.stringify(evaluated));
|
|
NODE
|
|
rm -f "$params_file" "$pipelines_file" "$artifact_file" "$argo_file" "$runtime_file"
|
|
}
|
|
|
|
hook_summary() {
|
|
hooks=$(gitea_api GET "repos/$UNIDESK_PAC_GITEA_OWNER/$UNIDESK_PAC_GITEA_REPO/hooks" 2>/dev/null || echo '[]')
|
|
HOOKS_JSON="$hooks" node <<'NODE'
|
|
const data = JSON.parse(process.env.HOOKS_JSON || '[]');
|
|
const url = process.env.UNIDESK_PAC_WEBHOOK_URL;
|
|
const rows = data.filter((item) => item?.config?.url === url).map((item) => ({ id: item.id, active: item.active, type: item.type, events: item.events || [], url: item.config?.url || '' }));
|
|
process.stdout.write(JSON.stringify(rows));
|
|
NODE
|
|
}
|
|
|
|
status_action() {
|
|
crd=$(kubectl get crd repositories.pipelinesascode.tekton.dev -o name 2>/dev/null || true)
|
|
controller_ready=$(kubectl -n "$UNIDESK_PAC_RELEASE_NAMESPACE" get deploy "$UNIDESK_PAC_CONTROLLER_SERVICE_NAME" -o jsonpath='{.status.readyReplicas}/{.spec.replicas}' 2>/dev/null || echo "0/0")
|
|
repository_condition=$(condition_status "$UNIDESK_PAC_TARGET_NAMESPACE" repository "$UNIDESK_PAC_REPOSITORY_NAME")
|
|
pipelines=$(pipeline_rows)
|
|
latest=$(printf '%s' "$pipelines" | node -e 'const fs=require("fs"); const a=JSON.parse(fs.readFileSync(0,"utf8")||"[]"); process.stdout.write(a[0]?.name||"")')
|
|
export UNIDESK_PAC_TARGET_PIPELINERUN="$latest"
|
|
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); 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' \
|
|
"$( [ -n "$crd" ] && [ "$controller_ready" != "0/0" ] && echo true || echo false )" \
|
|
"$( [ -n "$crd" ] && echo true || echo false )" \
|
|
"$(json_string "$controller_ready")" \
|
|
"$(json_string "$UNIDESK_PAC_REPOSITORY_NAME")" \
|
|
"$(json_string "$UNIDESK_PAC_GITEA_OWNER")" \
|
|
"$(json_string "$UNIDESK_PAC_GITEA_REPO")" \
|
|
"$(json_string "$UNIDESK_PAC_REPOSITORY_URL")" \
|
|
"$(json_string "$repository_condition")" \
|
|
"$(json_string "$repository_condition")" \
|
|
"$hooks" "$pipelines" "$tasks" "$artifact" "$argo" "$runtime" "$diagnostics"
|
|
}
|
|
|
|
history_action() {
|
|
crd=$(kubectl get crd repositories.pipelinesascode.tekton.dev -o name 2>/dev/null || true)
|
|
controller_ready=$(kubectl -n "$UNIDESK_PAC_RELEASE_NAMESPACE" get deploy "$UNIDESK_PAC_CONTROLLER_SERVICE_NAME" -o jsonpath='{.status.readyReplicas}/{.spec.replicas}' 2>/dev/null || echo "0/0")
|
|
repository_condition=$(condition_status "$UNIDESK_PAC_TARGET_NAMESPACE" repository "$UNIDESK_PAC_REPOSITORY_NAME")
|
|
hooks=$(hook_summary)
|
|
history=$(history_rows)
|
|
rows=$(printf '%s' "$history" | node -e 'const fs=require("fs"); const h=JSON.parse(fs.readFileSync(0,"utf8")||"{}"); process.stdout.write(JSON.stringify(h.rows||[]));')
|
|
consumers=$(printf '%s' "$history" | node -e 'const fs=require("fs"); const h=JSON.parse(fs.readFileSync(0,"utf8")||"{}"); process.stdout.write(JSON.stringify(h.consumers||[]));')
|
|
errors=$(printf '%s' "$history" | node -e 'const fs=require("fs"); const h=JSON.parse(fs.readFileSync(0,"utf8")||"{}"); process.stdout.write(JSON.stringify(h.errors||[]));')
|
|
printf '{"ok":%s,"crdPresent":%s,"controllerReady":"%s","repositoryCondition":"%s","repository":{"name":"%s","repo":"%s/%s","url":"%s"},"consumer":"%s","display":{"timeZone":"%s"},"consumerRows":%s,"rows":%s,"historyErrors":%s,"webhooks":%s,"source":"gitea-pac-tekton-live","historyStore":"none","valuesPrinted":false}\n' \
|
|
"$( [ -n "$crd" ] && [ "$controller_ready" != "0/0" ] && echo true || echo false )" \
|
|
"$( [ -n "$crd" ] && echo true || echo false )" \
|
|
"$(json_string "$controller_ready")" \
|
|
"$(json_string "$repository_condition")" \
|
|
"$(json_string "$UNIDESK_PAC_REPOSITORY_NAME")" \
|
|
"$(json_string "$UNIDESK_PAC_GITEA_OWNER")" \
|
|
"$(json_string "$UNIDESK_PAC_GITEA_REPO")" \
|
|
"$(json_string "$UNIDESK_PAC_REPOSITORY_URL")" \
|
|
"$(json_string "$UNIDESK_PAC_CONSUMER_ID")" \
|
|
"$(json_string "$UNIDESK_PAC_DISPLAY_TIME_ZONE")" \
|
|
"$consumers" "$rows" "$errors" "$hooks"
|
|
}
|
|
|
|
debug_step_action() {
|
|
fixtures=$(node <<'NODE'
|
|
const { runPacStatusFixtureChecks } = require(process.env.UNIDESK_PAC_EVALUATOR_PATH);
|
|
const result = runPacStatusFixtureChecks();
|
|
process.stdout.write(JSON.stringify(result));
|
|
NODE
|
|
)
|
|
if [ -z "$UNIDESK_PAC_HISTORY_ID" ]; then
|
|
printf '%s\n' "$fixtures"
|
|
FIXTURES_JSON="$fixtures" node -e 'const result=JSON.parse(process.env.FIXTURES_JSON||"{}"); if(!result.ok) process.exitCode=1'
|
|
return
|
|
fi
|
|
history=$(history_rows)
|
|
UNIDESK_PAC_DEBUG_FIXTURES_JSON="$fixtures" UNIDESK_PAC_DEBUG_HISTORY_JSON="$history" node <<'NODE'
|
|
function record(value) {
|
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
}
|
|
function terminalRow(sourceObservation, key, step) {
|
|
const source = record(record(sourceObservation.terminalSources)[key]);
|
|
return {
|
|
step,
|
|
present: source.present === true,
|
|
status: source.status || 'missing',
|
|
source: source.source || null,
|
|
taskRun: source.taskRun || null,
|
|
reason: source.reason || null,
|
|
results: Array.isArray(source.results) ? source.results : [],
|
|
markerPresent: source.markerPresent === true,
|
|
markerSource: source.markerSource || null,
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
const fixtures = JSON.parse(process.env.UNIDESK_PAC_DEBUG_FIXTURES_JSON || '{}');
|
|
const history = JSON.parse(process.env.UNIDESK_PAC_DEBUG_HISTORY_JSON || '{}');
|
|
const rows = Array.isArray(history.rows) ? history.rows : [];
|
|
const historyErrors = Array.isArray(history.errors) ? history.errors : [];
|
|
const row = record(rows[0]);
|
|
const sourceObservation = record(row.sourceObservation);
|
|
const artifact = record(row.artifact);
|
|
const catalog = record(artifact.catalog);
|
|
const collector = record(row.collector);
|
|
const terminal = [
|
|
terminalRow(sourceObservation, 'planArtifacts', 'plan-artifacts'),
|
|
terminalRow(sourceObservation, 'collectArtifacts', 'collect-artifacts'),
|
|
terminalRow(sourceObservation, 'gitopsPromote', 'gitops-promote'),
|
|
];
|
|
let firstBreak = null;
|
|
function breakAt(code, stage, reason) {
|
|
if (firstBreak === null) firstBreak = { code, stage, reason, valuesPrinted: false };
|
|
}
|
|
if (historyErrors.length > 0) breakAt('target-read-failed', 'target-read', historyErrors[0]?.error || historyErrors[0]?.stderr || 'target-side history read failed');
|
|
if (Object.keys(row).length === 0) breakAt('pipeline-run-not-found', 'pipeline-run', `PipelineRun ${process.env.UNIDESK_PAC_HISTORY_ID || '-'} was not found for the selected consumer`);
|
|
if (Object.keys(row).length > 0 && row.status !== 'True') breakAt('pipeline-run-not-succeeded', 'pipeline-run', `PipelineRun terminal status is ${row.status || row.reason || 'unknown'}`);
|
|
if (Object.keys(row).length > 0 && Object.keys(sourceObservation).length === 0) breakAt('source-observation-missing', 'plan-artifacts', 'g14-ci-plan structured evidence is missing');
|
|
if (sourceObservation.sourceMatched !== true) breakAt('source-observation-mismatch', 'plan-artifacts', 'g14-ci-plan source commit does not match the selected PipelineRun');
|
|
if (sourceObservation.valid !== true) breakAt('source-observation-invalid', 'plan-artifacts', sourceObservation.reason || 'delivery plan is incomplete or contradictory');
|
|
for (const item of terminal) {
|
|
if (item.present !== true || item.status !== 'succeeded') breakAt('terminal-evidence-incomplete', item.step, `${item.step} TaskRun terminal status is ${item.status}`);
|
|
}
|
|
if (Number(collector.logErrorCount || 0) > 0) breakAt('contract-log-read-failed', 'collector', 'one or more contract TaskRun logs could not be read');
|
|
if (sourceObservation.mode === 'delivery') {
|
|
if (catalog.present !== true || catalog.status !== 'published') breakAt('artifact-catalog-missing', 'collect-artifacts', 'published artifact catalog evidence is missing');
|
|
if (catalog.registryVerified !== true) breakAt('artifact-catalog-unverified', 'collect-artifacts', 'artifact catalog registry verification is not true');
|
|
if (!Array.isArray(artifact.digests) || artifact.digests.length === 0) breakAt('artifact-digest-missing', 'collect-artifacts', 'artifact catalog did not expose any sha256 digest');
|
|
if (typeof artifact.gitopsCommit !== 'string' || artifact.gitopsCommit.length === 0) breakAt('gitops-commit-missing', 'gitops-promote', 'GitOps commit is not visible in the successful promotion TaskRun');
|
|
}
|
|
const realRun = {
|
|
ok: firstBreak === null,
|
|
found: Object.keys(row).length > 0,
|
|
pipelineRun: Object.keys(row).length === 0 ? null : {
|
|
id: row.id || row.pipelineRun || null,
|
|
status: row.status || null,
|
|
reason: row.reason || null,
|
|
sourceCommit: row.commit || null,
|
|
sourceMatched: sourceObservation.sourceMatched === true,
|
|
valuesPrinted: false,
|
|
},
|
|
mode: sourceObservation.mode || null,
|
|
valid: sourceObservation.valid === true,
|
|
terminal,
|
|
artifact: {
|
|
imageStatus: artifact.imageStatus || null,
|
|
envReuse: artifact.envReuse || null,
|
|
digests: Array.isArray(artifact.digests) ? artifact.digests : [],
|
|
gitopsCommit: artifact.gitopsCommit || null,
|
|
sourceCommit: artifact.sourceCommit || null,
|
|
catalog: {
|
|
present: catalog.present === true,
|
|
status: catalog.status || null,
|
|
sourceCommit: catalog.sourceCommit || null,
|
|
registryVerified: catalog.registryVerified === true,
|
|
publishedCount: catalog.publishedCount ?? null,
|
|
reusedCount: catalog.reusedCount ?? null,
|
|
requiredServiceCount: catalog.requiredServiceCount ?? null,
|
|
digestCount: catalog.digestCount ?? null,
|
|
source: catalog.source || null,
|
|
valuesPrinted: false,
|
|
},
|
|
valuesPrinted: false,
|
|
},
|
|
collector: {
|
|
mode: collector.mode || null,
|
|
contractTaskCount: collector.contractTaskCount ?? null,
|
|
terminalRecordCount: collector.terminalRecordCount ?? null,
|
|
logErrorCount: collector.logErrorCount ?? null,
|
|
logErrors: Array.isArray(collector.logErrors) ? collector.logErrors : [],
|
|
valuesPrinted: false,
|
|
},
|
|
firstBreak,
|
|
valuesPrinted: false,
|
|
};
|
|
const out = {
|
|
ok: fixtures.ok === true && realRun.ok,
|
|
checks: Array.isArray(fixtures.checks) ? fixtures.checks : [],
|
|
realRun,
|
|
valuesPrinted: false,
|
|
};
|
|
process.stdout.write(`${JSON.stringify(out)}\n`);
|
|
if (!out.ok) process.exitCode = 1;
|
|
NODE
|
|
}
|
|
|
|
webhook_test_action() {
|
|
hooks=$(hook_summary)
|
|
hook_id=$(printf '%s' "$hooks" | node -e 'const fs=require("fs"); const a=JSON.parse(fs.readFileSync(0,"utf8")||"[]"); process.stdout.write(String(a[0]?.id||""))')
|
|
test -n "$hook_id"
|
|
gitea_api POST "repos/$UNIDESK_PAC_GITEA_OWNER/$UNIDESK_PAC_GITEA_REPO/hooks/$hook_id/tests" '{}' >/dev/null
|
|
printf '{"ok":true,"mutation":true,"webhookTestSent":true,"hookId":"%s","valuesPrinted":false}\n' "$(json_string "$hook_id")"
|
|
}
|
|
|
|
case "$UNIDESK_PAC_ACTION" in
|
|
apply) apply_action ;;
|
|
status) status_action ;;
|
|
history) history_action ;;
|
|
debug-step) debug_step_action ;;
|
|
webhook-test) webhook_test_action ;;
|
|
*) printf '{"ok":false,"error":"unsupported-action","valuesPrinted":false}\n'; exit 2 ;;
|
|
esac
|