From 56bf2dfa6906d635f6d2c9ac16f6ccb4b491d9d4 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 5 Jun 2026 01:19:50 +0000 Subject: [PATCH] feat(hwlab): bootstrap v02 OpenFGA prerequisites --- scripts/hwlab-g14-contract-test.ts | 12 + scripts/src/hwlab-g14.ts | 407 ++++++++++++++++++++++++++++- 2 files changed, 407 insertions(+), 12 deletions(-) diff --git a/scripts/hwlab-g14-contract-test.ts b/scripts/hwlab-g14-contract-test.ts index bbb87dee..bb913ae4 100644 --- a/scripts/hwlab-g14-contract-test.ts +++ b/scripts/hwlab-g14-contract-test.ts @@ -48,6 +48,18 @@ assertCondition( "v0.2 PR monitor help must expose the auto CI/CD lane, status query, latest-only CD, and dedupe comment state", hwlabG14Help(), ); +assertCondition( + hwlabHelpUsage.some((line) => line.includes("secret status --lane v02 --name hwlab-v02-openfga")) + && hwlabHelpUsage.some((line) => line.includes("secret ensure --lane v02 --name hwlab-v02-openfga --confirm")), + "v0.2 secret help must expose the controlled OpenFGA SecretRef bootstrap path", + hwlabHelpUsage, +); +assertCondition( + hwlabHelpUsage.some((line) => line.includes("upstream-image status --name openfga --tag v1.17.0")) + && hwlabHelpUsage.some((line) => line.includes("upstream-image ensure --name openfga --tag v1.17.0 --confirm")), + "v0.2 help must expose the controlled OpenFGA upstream image mirroring path", + hwlabHelpUsage, +); const v02CommentBody = v02PrAutomationCommentBody({ pr: { diff --git a/scripts/src/hwlab-g14.ts b/scripts/src/hwlab-g14.ts index 8581661f..1af3426d 100644 --- a/scripts/src/hwlab-g14.ts +++ b/scripts/src/hwlab-g14.ts @@ -32,6 +32,12 @@ const V02_RUNTIME_PATH = "deploy/gitops/g14/runtime-v02"; const V02_RUNTIME_NAMESPACE = "hwlab-v02"; const V02_DEVICE_POD_API_KEY_SECRET = "hwlab-v02-device-pod-api-key"; const V02_DEVICE_POD_API_KEY_SECRET_KEY = "api-key"; +const V02_OPENFGA_SECRET = "hwlab-v02-openfga"; +const V02_OPENFGA_AUTHN_SECRET_KEY = "authn-preshared-key"; +const V02_OPENFGA_DATASTORE_URI_SECRET_KEY = "datastore-uri"; +const V02_OPENFGA_POSTGRES_PASSWORD_SECRET_KEY = "postgres-password"; +const V02_OPENFGA_DB_NAME = "hwlab_openfga"; +const V02_OPENFGA_DB_USER = "hwlab_openfga"; const V02_REGISTRY_PREFIX = "127.0.0.1:5000/hwlab"; const V02_BASE_IMAGE = "127.0.0.1:5000/hwlab/hwlab-node20-base:20-bookworm-slim"; const GIT_MIRROR_NAMESPACE = "devops-infra"; @@ -131,6 +137,15 @@ interface G14ToolsImageOptions { timeoutSeconds: number; } +interface G14UpstreamImageOptions { + action: "status" | "ensure"; + name: "openfga"; + tag: string; + dryRun: boolean; + confirm: boolean; + timeoutSeconds: number; +} + interface G14GitMirrorOptions { action: "status" | "apply" | "sync" | "flush"; dryRun: boolean; @@ -153,8 +168,9 @@ interface G14SecretOptions { lane: "v02"; dryRun: boolean; confirm: boolean; - name: typeof V02_DEVICE_POD_API_KEY_SECRET; - key: typeof V02_DEVICE_POD_API_KEY_SECRET_KEY; + name: typeof V02_DEVICE_POD_API_KEY_SECRET | typeof V02_OPENFGA_SECRET; + key?: typeof V02_DEVICE_POD_API_KEY_SECRET_KEY | typeof V02_OPENFGA_AUTHN_SECRET_KEY | typeof V02_OPENFGA_DATASTORE_URI_SECRET_KEY | typeof V02_OPENFGA_POSTGRES_PASSWORD_SECRET_KEY; + preset: "device-pod-api-key" | "openfga"; timeoutSeconds: number; } @@ -370,6 +386,28 @@ function parseToolsImageOptions(args: string[]): G14ToolsImageOptions { }; } +function parseUpstreamImageOptions(args: string[]): G14UpstreamImageOptions { + const [actionRaw] = args; + if (actionRaw !== "status" && actionRaw !== "ensure") { + throw new Error("upstream-image usage: status|ensure --name openfga --tag v1.17.0 [--dry-run|--confirm]"); + } + const name = optionValue(args, "--name") ?? "openfga"; + if (name !== "openfga") throw new Error("upstream-image currently supports --name openfga"); + const tag = optionValue(args, "--tag") ?? "v1.17.0"; + if (tag !== "v1.17.0") throw new Error("upstream-image currently supports OpenFGA tag v1.17.0"); + const confirm = args.includes("--confirm"); + const explicitDryRun = args.includes("--dry-run"); + if (confirm && explicitDryRun) throw new Error("upstream-image accepts only one of --confirm or --dry-run"); + return { + action: actionRaw, + name, + tag, + confirm, + dryRun: actionRaw === "status" ? true : explicitDryRun || !confirm, + timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", 600, 1800), + }; +} + function parseGitMirrorOptions(args: string[]): G14GitMirrorOptions { const [actionRaw] = args; if (actionRaw !== "status" && actionRaw !== "apply" && actionRaw !== "sync" && actionRaw !== "flush") { @@ -411,14 +449,22 @@ function parseObservabilityOptions(args: string[]): G14ObservabilityOptions { function parseSecretOptions(args: string[]): G14SecretOptions { const [actionRaw] = args; if (actionRaw !== "status" && actionRaw !== "ensure") { - throw new Error("secret usage: status|ensure --lane v02 --name hwlab-v02-device-pod-api-key --key api-key [--dry-run|--confirm]"); + throw new Error("secret usage: status|ensure --lane v02 --name hwlab-v02-device-pod-api-key --key api-key | --name hwlab-v02-openfga [--dry-run|--confirm]"); } const lane = optionValue(args, "--lane") ?? "v02"; if (lane !== "v02") throw new Error("secret currently supports --lane v02"); const name = optionValue(args, "--name") ?? V02_DEVICE_POD_API_KEY_SECRET; - if (name !== V02_DEVICE_POD_API_KEY_SECRET) throw new Error(`secret currently supports --name ${V02_DEVICE_POD_API_KEY_SECRET}`); - const key = optionValue(args, "--key") ?? V02_DEVICE_POD_API_KEY_SECRET_KEY; - if (key !== V02_DEVICE_POD_API_KEY_SECRET_KEY) throw new Error(`secret currently supports --key ${V02_DEVICE_POD_API_KEY_SECRET_KEY}`); + if (name !== V02_DEVICE_POD_API_KEY_SECRET && name !== V02_OPENFGA_SECRET) { + throw new Error(`secret currently supports --name ${V02_DEVICE_POD_API_KEY_SECRET} or ${V02_OPENFGA_SECRET}`); + } + const key = optionValue(args, "--key"); + const preset = name === V02_OPENFGA_SECRET ? "openfga" : "device-pod-api-key"; + if (preset === "device-pod-api-key") { + const effectiveKey = key ?? V02_DEVICE_POD_API_KEY_SECRET_KEY; + if (effectiveKey !== V02_DEVICE_POD_API_KEY_SECRET_KEY) throw new Error(`secret ${V02_DEVICE_POD_API_KEY_SECRET} supports --key ${V02_DEVICE_POD_API_KEY_SECRET_KEY}`); + } else if (key !== undefined && key !== V02_OPENFGA_AUTHN_SECRET_KEY && key !== V02_OPENFGA_DATASTORE_URI_SECRET_KEY && key !== V02_OPENFGA_POSTGRES_PASSWORD_SECRET_KEY) { + throw new Error(`secret ${V02_OPENFGA_SECRET} supports keys ${V02_OPENFGA_AUTHN_SECRET_KEY}, ${V02_OPENFGA_DATASTORE_URI_SECRET_KEY}, and ${V02_OPENFGA_POSTGRES_PASSWORD_SECRET_KEY}`); + } const confirm = args.includes("--confirm"); const explicitDryRun = args.includes("--dry-run"); if (confirm && explicitDryRun) throw new Error("secret accepts only one of --confirm or --dry-run"); @@ -428,7 +474,8 @@ function parseSecretOptions(args: string[]): G14SecretOptions { confirm, dryRun: actionRaw === "status" ? true : explicitDryRun || !confirm, name, - key, + key: preset === "device-pod-api-key" ? V02_DEVICE_POD_API_KEY_SECRET_KEY : key, + preset, timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", 120, 600), }; } @@ -3193,6 +3240,8 @@ function runV02ControlPlane(options: G14ControlPlaneOptions): Record/dev/null 2>&1 && printf yes || printf no; }", + "secret_b64_key() { kubectl -n \"$namespace\" get secret \"$name\" -o \"go-template={{ index .data \\\"$1\\\" }}\" 2>/dev/null || true; }", + "decoded_value() { if [ -n \"$1\" ]; then printf '%s' \"$1\" | base64 -d 2>/dev/null || true; fi; }", + "decoded_length() { if [ -n \"$1\" ]; then printf '%s' \"$1\" | base64 -d 2>/dev/null | wc -c | tr -d ' '; else printf '0'; fi; }", + "psql_scalar() { kubectl -n \"$namespace\" exec statefulset/hwlab-v02-postgres -c postgres -- env PGPASSWORD=\"$postgres_admin_password\" psql -U hwlab_v02 -d postgres -tAc \"$1\" 2>/dev/null | tr -d '[:space:]'; }", + "probe_db() {", + " role_result=unknown", + " database_result=unknown", + " probe_exit=missing-postgres-admin-secret", + " if [ -n \"$postgres_admin_password\" ]; then", + ` role_result=$(psql_scalar "select exists(select 1 from pg_roles where rolname='${V02_OPENFGA_DB_USER}');")`, + " role_exit=$?", + ` database_result=$(psql_scalar "select exists(select 1 from pg_database where datname='${V02_OPENFGA_DB_NAME}');")`, + " database_exit=$?", + " if [ \"$role_exit\" -eq 0 ] && [ \"$database_exit\" -eq 0 ]; then probe_exit=0; else probe_exit=$role_exit/$database_exit; fi", + " fi", + "}", + "before_exists=$(secret_exists_flag)", + "before_authn_b64=$(secret_b64_key \"$authn_key\")", + "before_uri_b64=$(secret_b64_key \"$datastore_uri_key\")", + "before_pg_password_b64=$(secret_b64_key \"$postgres_password_key\")", + "before_authn_present=$([ -n \"$before_authn_b64\" ] && printf yes || printf no)", + "before_uri_present=$([ -n \"$before_uri_b64\" ] && printf yes || printf no)", + "before_pg_password_present=$([ -n \"$before_pg_password_b64\" ] && printf yes || printf no)", + "before_authn_bytes=$(decoded_length \"$before_authn_b64\")", + "before_uri_bytes=$(decoded_length \"$before_uri_b64\")", + "before_pg_password_bytes=$(decoded_length \"$before_pg_password_b64\")", + "authn_value=$(decoded_value \"$before_authn_b64\")", + "datastore_uri=$(decoded_value \"$before_uri_b64\")", + "pg_password=$(decoded_value \"$before_pg_password_b64\")", + "postgres_admin_b64=$(kubectl -n \"$namespace\" get secret hwlab-v02-postgres -o 'go-template={{ index .data \"POSTGRES_PASSWORD\" }}' 2>/dev/null || true)", + "postgres_admin_present=$([ -n \"$postgres_admin_b64\" ] && printf yes || printf no)", + "postgres_admin_password=$(decoded_value \"$postgres_admin_b64\")", + "probe_db", + "db_role_exists_before=$role_result", + "db_database_exists_before=$database_result", + "db_probe_exit_before=$probe_exit", + "action=observed", + "mutation=false", + "apply_exit=", + "db_ensure_exit=", + "if [ \"$action_request\" = ensure ]; then", + " missing_secret=false", + " [ \"$before_authn_present\" = yes ] && [ \"$before_authn_bytes\" -gt 0 ] || missing_secret=true", + " [ \"$before_uri_present\" = yes ] && [ \"$before_uri_bytes\" -gt 0 ] || missing_secret=true", + " [ \"$before_pg_password_present\" = yes ] && [ \"$before_pg_password_bytes\" -gt 0 ] || missing_secret=true", + " missing_db=false", + " [ \"$db_role_exists_before\" = t ] || missing_db=true", + " [ \"$db_database_exists_before\" = t ] || missing_db=true", + " if [ \"$dry_run\" = true ]; then", + " if [ \"$missing_secret\" = true ] || [ \"$missing_db\" = true ]; then action=would-ensure; else action=kept; fi", + " elif ! command -v openssl >/dev/null 2>&1; then", + " action=openssl-missing", + " apply_exit=127", + " elif [ -z \"$postgres_admin_password\" ]; then", + " action=postgres-admin-secret-missing", + " apply_exit=44", + " else", + " [ -n \"$authn_value\" ] || authn_value=$(openssl rand -base64 48)", + " [ -n \"$pg_password\" ] || pg_password=$(openssl rand -hex 24)", + " [ -n \"$datastore_uri\" ] || datastore_uri=\"postgres://$db_user:$pg_password@$db_host:5432/$db_name?sslmode=disable\"", + " kubectl -n \"$namespace\" create secret generic \"$name\" --from-literal=\"$authn_key=$authn_value\" --from-literal=\"$datastore_uri_key=$datastore_uri\" --from-literal=\"$postgres_password_key=$pg_password\" --dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=\"$field_manager\" -f -", + " apply_exit=$?", + " if [ \"$apply_exit\" -eq 0 ]; then", + " kubectl -n \"$namespace\" exec -i statefulset/hwlab-v02-postgres -c postgres -- env PGPASSWORD=\"$postgres_admin_password\" psql -v ON_ERROR_STOP=1 -U hwlab_v02 -d postgres -v db_name=\"$db_name\" -v db_user=\"$db_user\" -v db_pass=\"$pg_password\" >/tmp/hwlab-openfga-psql.out 2>/tmp/hwlab-openfga-psql.err <<'SQL'", + "SELECT format('CREATE ROLE %I LOGIN PASSWORD %L', :'db_user', :'db_pass')", + "WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = :'db_user')", + "\\gexec", + "ALTER ROLE :\"db_user\" LOGIN PASSWORD :'db_pass';", + "SELECT format('CREATE DATABASE %I OWNER %I', :'db_name', :'db_user')", + "WHERE NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = :'db_name')", + "\\gexec", + "ALTER DATABASE :\"db_name\" OWNER TO :\"db_user\";", + "SQL", + " db_ensure_exit=$?", + " if [ \"$db_ensure_exit\" -eq 0 ]; then action=ensured; mutation=true; else action=db-ensure-failed; fi", + " else", + " action=apply-failed", + " fi", + " fi", + "fi", + "after_exists=$(secret_exists_flag)", + "after_authn_b64=$(secret_b64_key \"$authn_key\")", + "after_uri_b64=$(secret_b64_key \"$datastore_uri_key\")", + "after_pg_password_b64=$(secret_b64_key \"$postgres_password_key\")", + "after_authn_present=$([ -n \"$after_authn_b64\" ] && printf yes || printf no)", + "after_uri_present=$([ -n \"$after_uri_b64\" ] && printf yes || printf no)", + "after_pg_password_present=$([ -n \"$after_pg_password_b64\" ] && printf yes || printf no)", + "after_authn_bytes=$(decoded_length \"$after_authn_b64\")", + "after_uri_bytes=$(decoded_length \"$after_uri_b64\")", + "after_pg_password_bytes=$(decoded_length \"$after_pg_password_b64\")", + "probe_db", + "db_role_exists_after=$role_result", + "db_database_exists_after=$database_result", + "db_probe_exit_after=$probe_exit", + "printf 'namespace\\t%s\\n' \"$namespace\"", + "printf 'secret\\t%s\\n' \"$name\"", + "printf 'key\\t%s\\n' \"$selected_key\"", + "printf 'preset\\t%s\\n' \"$preset\"", + "printf 'action\\t%s\\n' \"$action\"", + "printf 'dryRun\\t%s\\n' \"$dry_run\"", + "printf 'mutation\\t%s\\n' \"$mutation\"", + "printf 'beforeExists\\t%s\\n' \"$before_exists\"", + "printf 'beforeAuthnPresent\\t%s\\n' \"$before_authn_present\"", + "printf 'beforeAuthnBytes\\t%s\\n' \"$before_authn_bytes\"", + "printf 'beforeDatastoreUriPresent\\t%s\\n' \"$before_uri_present\"", + "printf 'beforeDatastoreUriBytes\\t%s\\n' \"$before_uri_bytes\"", + "printf 'beforePostgresPasswordPresent\\t%s\\n' \"$before_pg_password_present\"", + "printf 'beforePostgresPasswordBytes\\t%s\\n' \"$before_pg_password_bytes\"", + "printf 'afterExists\\t%s\\n' \"$after_exists\"", + "printf 'afterAuthnPresent\\t%s\\n' \"$after_authn_present\"", + "printf 'afterAuthnBytes\\t%s\\n' \"$after_authn_bytes\"", + "printf 'afterDatastoreUriPresent\\t%s\\n' \"$after_uri_present\"", + "printf 'afterDatastoreUriBytes\\t%s\\n' \"$after_uri_bytes\"", + "printf 'afterPostgresPasswordPresent\\t%s\\n' \"$after_pg_password_present\"", + "printf 'afterPostgresPasswordBytes\\t%s\\n' \"$after_pg_password_bytes\"", + "printf 'postgresAdminSecretPresent\\t%s\\n' \"$postgres_admin_present\"", + "printf 'dbName\\t%s\\n' \"$db_name\"", + "printf 'dbUser\\t%s\\n' \"$db_user\"", + "printf 'dbRoleExistsBefore\\t%s\\n' \"$db_role_exists_before\"", + "printf 'dbDatabaseExistsBefore\\t%s\\n' \"$db_database_exists_before\"", + "printf 'dbProbeExitCodeBefore\\t%s\\n' \"$db_probe_exit_before\"", + "printf 'dbRoleExistsAfter\\t%s\\n' \"$db_role_exists_after\"", + "printf 'dbDatabaseExistsAfter\\t%s\\n' \"$db_database_exists_after\"", + "printf 'dbProbeExitCodeAfter\\t%s\\n' \"$db_probe_exit_after\"", + "printf 'applyExitCode\\t%s\\n' \"$apply_exit\"", + "printf 'dbEnsureExitCode\\t%s\\n' \"$db_ensure_exit\"", + "authn_value=", + "datastore_uri=", + "pg_password=", + "postgres_admin_password=", + "if [ -n \"$apply_exit\" ] && [ \"$apply_exit\" != 0 ]; then exit \"$apply_exit\"; fi", + "if [ -n \"$db_ensure_exit\" ] && [ \"$db_ensure_exit\" != 0 ]; then exit \"$db_ensure_exit\"; fi", + ].join("\n"); +} + function v02SecretStatusFromText(text: string, commandOk: boolean, exitCode: number | null, stderr: string): Record { const fields = keyValueLinesFromText(text); + if (fields.preset === "openfga") { + const afterAuthnBytes = numericField(fields.afterAuthnBytes); + const afterUriBytes = numericField(fields.afterDatastoreUriBytes); + const afterPasswordBytes = numericField(fields.afterPostgresPasswordBytes); + const keysHealthy = + fields.afterExists === "yes" && + fields.afterAuthnPresent === "yes" && + fields.afterDatastoreUriPresent === "yes" && + fields.afterPostgresPasswordPresent === "yes" && + typeof afterAuthnBytes === "number" && afterAuthnBytes > 0 && + typeof afterUriBytes === "number" && afterUriBytes > 0 && + typeof afterPasswordBytes === "number" && afterPasswordBytes > 0; + const databaseHealthy = fields.dbRoleExistsAfter === "t" && fields.dbDatabaseExistsAfter === "t"; + const healthy = keysHealthy && databaseHealthy; + return { + ok: commandOk && healthy, + namespace: fields.namespace || V02_RUNTIME_NAMESPACE, + secret: fields.secret || V02_OPENFGA_SECRET, + key: fields.key || null, + preset: "openfga", + action: fields.action || null, + dryRun: fields.dryRun === "true", + mutation: fields.mutation === "true", + before: { + exists: fields.beforeExists === "yes", + authnPresharedKey: { keyPresent: fields.beforeAuthnPresent === "yes", valueBytes: numericField(fields.beforeAuthnBytes) }, + datastoreUri: { keyPresent: fields.beforeDatastoreUriPresent === "yes", valueBytes: numericField(fields.beforeDatastoreUriBytes) }, + postgresPassword: { keyPresent: fields.beforePostgresPasswordPresent === "yes", valueBytes: numericField(fields.beforePostgresPasswordBytes) }, + database: { + roleExists: fields.dbRoleExistsBefore || "unknown", + databaseExists: fields.dbDatabaseExistsBefore || "unknown", + probeExitCode: fields.dbProbeExitCodeBefore || null, + }, + }, + after: { + exists: fields.afterExists === "yes", + authnPresharedKey: { keyPresent: fields.afterAuthnPresent === "yes", valueBytes: afterAuthnBytes }, + datastoreUri: { keyPresent: fields.afterDatastoreUriPresent === "yes", valueBytes: afterUriBytes }, + postgresPassword: { keyPresent: fields.afterPostgresPasswordPresent === "yes", valueBytes: afterPasswordBytes }, + database: { + roleExists: fields.dbRoleExistsAfter || "unknown", + databaseExists: fields.dbDatabaseExistsAfter || "unknown", + probeExitCode: fields.dbProbeExitCodeAfter || null, + }, + }, + postgresAdminSecretPresent: fields.postgresAdminSecretPresent === "yes", + dbName: fields.dbName || V02_OPENFGA_DB_NAME, + dbUser: fields.dbUser || V02_OPENFGA_DB_USER, + applyExitCode: numericField(fields.applyExitCode), + dbEnsureExitCode: numericField(fields.dbEnsureExitCode), + exitCode, + stderr: commandOk ? "" : stderr.trim().slice(0, 2000), + valuesRedacted: true, + summary: healthy + ? `${fields.secret || V02_OPENFGA_SECRET} keys and Postgres database exist` + : `${fields.secret || V02_OPENFGA_SECRET} keys or Postgres database missing`, + }; + } const afterExists = fields.afterExists === "yes"; const afterKeyPresent = fields.afterKeyPresent === "yes"; const afterValueBytes = numericField(fields.afterValueBytes); @@ -3299,7 +3560,8 @@ function runG14Secret(options: G14SecretOptions): Record { lane: options.lane, namespace: V02_RUNTIME_NAMESPACE, secret: options.name, - key: options.key, + key: options.key ?? null, + preset: options.preset, mode: options.action === "status" ? "status" : options.dryRun ? "dry-run" : "confirmed-ensure", status, mutation: status.mutation === true, @@ -3307,7 +3569,7 @@ function runG14Secret(options: G14SecretOptions): Record { valuesRedacted: true, next: ok && options.action === "status" ? undefined - : { ensure: `bun scripts/cli.ts hwlab g14 secret ensure --lane v02 --name ${options.name} --key ${options.key} --confirm` }, + : { ensure: `bun scripts/cli.ts hwlab g14 secret ensure --lane v02 --name ${options.name}${options.key ? ` --key ${options.key}` : ""} --confirm` }, }; } @@ -4837,6 +5099,117 @@ function runG14ToolsImage(options: G14ToolsImageOptions): Record { + const sourceImage = g14UpstreamImageSource(options); + const targetImage = g14UpstreamImageTarget(options); + const script = [ + "set +e", + `source_image=${shellQuote(sourceImage)}`, + `target_image=${shellQuote(targetImage)}`, + `repo_path=${shellQuote(`hwlab/${options.name}`)}`, + `tag=${shellQuote(options.tag)}`, + "local_id=$(docker image inspect \"$target_image\" --format '{{.Id}}' 2>/dev/null || true)", + "local_created=$(docker image inspect \"$target_image\" --format '{{.Created}}' 2>/dev/null || true)", + "registry_tags=$(curl -fsS --max-time 10 \"http://127.0.0.1:5000/v2/$repo_path/tags/list\" 2>/dev/null || true)", + "registry_has_tag=false", + "if printf '%s' \"$registry_tags\" | grep -F '\"'$tag'\"' >/dev/null 2>&1; then registry_has_tag=true; fi", + "export source_image target_image local_id local_created registry_tags registry_has_tag", + "node <<'NODE'", + "const env = process.env;", + "console.log(JSON.stringify({", + " sourceImage: env.source_image,", + " targetImage: env.target_image,", + " localExists: Boolean(env.local_id),", + " localImageId: env.local_id || null,", + " localCreated: env.local_created || null,", + " registryHasTag: env.registry_has_tag === 'true',", + " registryTagsRaw: env.registry_tags || null", + "}, null, 2));", + "NODE", + ].join("\n"); + const result = g14HostScript(script, 120_000); + let parsedStatus: unknown = null; + const text = statusText(result); + if (text.length > 0) { + try { + parsedStatus = JSON.parse(text) as unknown; + } catch { + parsedStatus = null; + } + } + return { + ok: isCommandSuccess(result) && record(parsedStatus).registryHasTag === true, + command: "hwlab g14 upstream-image status --name openfga", + name: options.name, + tag: options.tag, + sourceImage, + targetImage, + status: parsedStatus ?? text, + result, + }; +} + +function runG14UpstreamImageEnsure(options: G14UpstreamImageOptions): Record { + const sourceImage = g14UpstreamImageSource(options); + const targetImage = g14UpstreamImageTarget(options); + if (options.dryRun) { + return { + ok: true, + command: "hwlab g14 upstream-image ensure --name openfga", + mode: "dry-run", + name: options.name, + tag: options.tag, + sourceImage, + targetImage, + mutation: false, + next: { confirm: `bun scripts/cli.ts hwlab g14 upstream-image ensure --name ${options.name} --tag ${options.tag} --confirm` }, + }; + } + const script = [ + "set -eu", + `source_image=${shellQuote(sourceImage)}`, + `target_image=${shellQuote(targetImage)}`, + "export HTTP_PROXY=http://127.0.0.1:10808 HTTPS_PROXY=http://127.0.0.1:10808 http_proxy=http://127.0.0.1:10808 https_proxy=http://127.0.0.1:10808", + "export NO_PROXY=localhost,127.0.0.1,::1,host.docker.internal,74.48.78.17,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,10.42.0.0/16,10.43.0.0/16,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,127.0.0.1:5000,localhost:5000", + "export no_proxy=$NO_PROXY", + "echo \"{\\\"phase\\\":\\\"pull\\\",\\\"sourceImage\\\":\\\"$source_image\\\"}\"", + "docker pull \"$source_image\"", + "docker tag \"$source_image\" \"$target_image\"", + "echo \"{\\\"phase\\\":\\\"push\\\",\\\"targetImage\\\":\\\"$target_image\\\"}\"", + "docker push \"$target_image\"", + "digest=$(docker image inspect \"$target_image\" --format '{{index .RepoDigests 0}}' 2>/dev/null || true)", + "echo \"{\\\"phase\\\":\\\"published\\\",\\\"targetImage\\\":\\\"$target_image\\\",\\\"digest\\\":\\\"$digest\\\"}\"", + ].join("\n"); + const result = g14HostScript(script, options.timeoutSeconds * 1000); + const status = runG14UpstreamImageStatus(options); + return { + ok: isCommandSuccess(result) && status.ok === true, + command: "hwlab g14 upstream-image ensure --name openfga", + mode: "confirmed-ensure", + name: options.name, + tag: options.tag, + sourceImage, + targetImage, + mutation: true, + result, + status, + }; +} + +function runG14UpstreamImage(options: G14UpstreamImageOptions): Record { + if (options.action === "status") return runG14UpstreamImageStatus(options); + return runG14UpstreamImageEnsure(options); +} + function listOpenG14PullRequests(): CommandJsonResult { return cliJson(["gh", "pr", "list", "--repo", HWLAB_REPO, "--state", "open", "--limit", "30", "--json", "number,title,state,url,head,base,draft,headRefName,baseRefName"], 60_000); } @@ -6167,6 +6540,9 @@ export function hwlabG14Help(): Record { "bun scripts/cli.ts hwlab g14 secret status --lane v02 --name hwlab-v02-device-pod-api-key --key api-key", "bun scripts/cli.ts hwlab g14 secret ensure --lane v02 --name hwlab-v02-device-pod-api-key --key api-key --dry-run", "bun scripts/cli.ts hwlab g14 secret ensure --lane v02 --name hwlab-v02-device-pod-api-key --key api-key --confirm", + "bun scripts/cli.ts hwlab g14 secret status --lane v02 --name hwlab-v02-openfga", + "bun scripts/cli.ts hwlab g14 secret ensure --lane v02 --name hwlab-v02-openfga --dry-run", + "bun scripts/cli.ts hwlab g14 secret ensure --lane v02 --name hwlab-v02-openfga --confirm", "bun scripts/cli.ts hwlab g14 git-mirror status", "bun scripts/cli.ts hwlab g14 git-mirror apply --confirm", "bun scripts/cli.ts hwlab g14 git-mirror sync --confirm", @@ -6179,9 +6555,12 @@ export function hwlabG14Help(): Record { "bun scripts/cli.ts hwlab g14 observability query --promql 'up{namespace=\"hwlab-v02\"}'", "bun scripts/cli.ts hwlab g14 tools-image status --name ci-node-tools --tag node22-alpine-bun-v1", "bun scripts/cli.ts hwlab g14 tools-image build --name ci-node-tools --tag node22-alpine-bun-v1 --confirm", + "bun scripts/cli.ts hwlab g14 upstream-image status --name openfga --tag v1.17.0", + "bun scripts/cli.ts hwlab g14 upstream-image ensure --name openfga --tag v1.17.0 --dry-run", + "bun scripts/cli.ts hwlab g14 upstream-image ensure --name openfga --tag v1.17.0 --confirm", "bun scripts/cli.ts job status --tail-bytes 30000", ], - description: "G14 HWLAB PR monitor, DEV rollout command, bounded v0.2 control-plane bootstrap/cleanup/runtime-migration helper, v0.2 runtime SecretRef bootstrap, devops-infra git mirror and observability maintenance, and controlled CI tools image build/status entry. The public monitor starts a fire-and-forget job. Default monitor lane is base=G14; --lane v02 monitors base=v0.2 PRs, waits for GitHub preflight/CI readiness, automatically merges ready PRs without waiting for other active v0.2 PipelineRuns, triggers v0.2 CD with latest-only GitOps writeback, flushes the git mirror when needed, and posts deduplicated PR comments for pending, blocked/conflict, success, superseded, failure, or timeout states. confirmed control-plane trigger-current and git-mirror sync/flush also return async jobs by default, with --wait reserved for explicit synchronous debugging. control-plane status/closeout/apply/cleanup-runs/cleanup-released-pvs/runtime-migration uses UniDesk G14:k3s routes for v0.2 Tekton/Argo control resources, runtime migration, historical PipelineRun/source-commit closeout verdicts, GitOps mirror flush state, and completed CI workspace retention only. secret status/ensure is the standard v0.2 runtime SecretRef bootstrap path; it never reads or prints secret values. git-mirror status/apply/sync/flush is the manual devops-infra mirror/relay control path and does not install a CronJob. observability status/apply/query owns the shared Prometheus Operator and Prometheus instance in devops-infra, while HWLAB lane manifests own only ServiceMonitor and PrometheusRule objects.", + description: "G14 HWLAB PR monitor, DEV rollout command, bounded v0.2 control-plane bootstrap/cleanup/runtime-migration helper, v0.2 runtime SecretRef bootstrap, devops-infra git mirror and observability maintenance, controlled CI tools image build/status entry, and allowlisted upstream image mirroring. The public monitor starts a fire-and-forget job. Default monitor lane is base=G14; --lane v02 monitors base=v0.2 PRs, waits for GitHub preflight/CI readiness, automatically merges ready PRs without waiting for other active v0.2 PipelineRuns, triggers v0.2 CD with latest-only GitOps writeback, flushes the git mirror when needed, and posts deduplicated PR comments for pending, blocked/conflict, success, superseded, failure, or timeout states. confirmed control-plane trigger-current and git-mirror sync/flush also return async jobs by default, with --wait reserved for explicit synchronous debugging. control-plane status/closeout/apply/cleanup-runs/cleanup-released-pvs/runtime-migration uses UniDesk G14:k3s routes for v0.2 Tekton/Argo control resources, runtime migration, historical PipelineRun/source-commit closeout verdicts, GitOps mirror flush state, and completed CI workspace retention only. secret status/ensure is the standard v0.2 runtime SecretRef bootstrap path; it never reads or prints secret values. upstream-image status/ensure only mirrors allowlisted upstream runtime images into the G14 local registry. git-mirror status/apply/sync/flush is the manual devops-infra mirror/relay control path and does not install a CronJob. observability status/apply/query owns the shared Prometheus Operator and Prometheus instance in devops-infra, while HWLAB lane manifests own only ServiceMonitor and PrometheusRule objects.", defaults: { repo: HWLAB_REPO, base: G14_SOURCE_BRANCH, @@ -6295,6 +6674,10 @@ export async function runHwlabG14Command(_config: Config, args: string[]): Promi const options = parseToolsImageOptions(args.slice(1)); return runG14ToolsImage(options); } + if (action === "upstream-image") { + const options = parseUpstreamImageOptions(args.slice(1)); + return runG14UpstreamImage(options); + } if (action === "git-mirror") { const options = parseGitMirrorOptions(args.slice(1)); if ((options.action === "sync" || options.action === "flush") && options.confirm && !options.dryRun && !options.wait) { @@ -6307,7 +6690,7 @@ export async function runHwlabG14Command(_config: Config, args: string[]): Promi return runG14Observability(options); } if (action !== "monitor-prs") { - return { ok: false, command: `hwlab g14 ${action ?? ""}`.trim(), degradedReason: "unsupported-command", message: "supported commands: hwlab g14 monitor-prs, hwlab g14 record-rollout, hwlab g14 control-plane, hwlab g14 secret, hwlab g14 git-mirror, hwlab g14 observability, hwlab g14 tools-image" }; + return { ok: false, command: `hwlab g14 ${action ?? ""}`.trim(), degradedReason: "unsupported-command", message: "supported commands: hwlab g14 monitor-prs, hwlab g14 record-rollout, hwlab g14 control-plane, hwlab g14 secret, hwlab g14 git-mirror, hwlab g14 observability, hwlab g14 tools-image, hwlab g14 upstream-image" }; } const options = parseOptions(args.slice(1)); if (options.worker) return runMonitorWorker(options);