#!/usr/bin/env nu # Pluggable selection of the OCI push-registry target, independent of the active # workspace. A command resolves the endpoint + credentials from a configurable # source — sops | api | literal | env — and materializes them as an env setting # (PROVISIONING_REGISTRY + PROVISIONING_PUSH_DOCKER_CONFIG) that the OCI contract # resolver consumes. The source is declared once (provisioning config # registry.push.*) or passed as flags; the command resolves it at call time. # # Authorization model: in override mode the credential SOURCE is the gate — # decrypting the sops file needs the Age key, the api call needs its own auth. # This replaces the workspace-scope check, which is what makes the push target # selectable beyond any workspace. use platform/config/accessor.nu * use platform/oci/credentials.nu [build-docker-config-tmpdir] # config-get wrapper tolerant of an unloaded provisioning config. def cfg [key: string, fallback: string]: nothing -> string { try { config-get $key $fallback | into string } catch { $fallback } } # Extract a dotted key path from a record (e.g. "data.registry.endpoint"). def get-path [data: record, dotted: string]: nothing -> string { let parts = ($dotted | split row ".") let val = ($parts | reduce --fold $data { |p, acc| if (($acc | describe) | str starts-with "record") { $acc | get -o $p | default "" } else { "" } }) if (($val | describe) | str starts-with "record") { "" } else { $val | to text } } def resolve-from-sops [ path: string, ekey: string, ukey: string, pkey: string, vault_id: string, age_key: string ]: nothing -> record { let sops_path = if ($path | path exists) { $path } else if ($vault_id | is-not-empty) { $env.HOME | path join ".config" "ontoref" "vaults" $vault_id "src-vault" $path } else { $path } if not ($sops_path | path exists) { error make --unspanned { msg: $"[push-registry-sops-missing] ($sops_path)" } } let kage = if ($age_key | is-not-empty) { $age_key } else { ($env.SOPS_AGE_KEY_FILE? | default "") } let r = if ($kage | is-not-empty) { with-env { SOPS_AGE_KEY_FILE: $kage } { do { ^sops --decrypt --output-type json $sops_path } | complete } } else { do { ^sops --decrypt --output-type json $sops_path } | complete } if $r.exit_code != 0 { error make --unspanned { msg: $"[push-registry-sops-decrypt-failed] ($r.stderr)" } } let data = ($r.stdout | from json) { endpoint: (get-path $data $ekey), username: (get-path $data $ukey), password: (get-path $data $pkey) } } def resolve-from-api [ url: string, ekey: string, ukey: string, pkey: string, auth_header: string ]: nothing -> record { let body = if ($auth_header | is-not-empty) { http get --headers ["Authorization" $auth_header] $url } else { http get $url } { endpoint: (get-path $body $ekey), username: (get-path $body $ukey), password: (get-path $body $pkey) } } # Resolve { endpoint, username, password } for the push registry from a pluggable # source. Precedence: explicit --kind flag → provisioning config registry.push.kind. export def resolve-push-registry [ --kind: string = "" # sops | api | literal | env --value: string = "" # literal endpoint | env var name | sops path | api url --endpoint-key: string = "endpoint" # sops key / api json path for endpoint --user-key: string = "username" --pass-key: string = "password" --vault-id: string = "" # sops vault id for decryption-key resolution --age-key: string = "" # explicit Age key file for sops --auth-header: string = "" # api Authorization header value ]: nothing -> record { let k = if ($kind | is-not-empty) { $kind } else { (cfg "registry.push.kind" "") } if ($k | is-empty) { error make --unspanned { msg: "push-registry source kind not set — pass --kind or set registry.push.kind in config" } } let v = if ($value | is-not-empty) { $value } else { (cfg "registry.push.value" "") } match $k { "literal" => { endpoint: $v, username: "", password: "" }, "env" => { let ep = ($env | get -o $v | default "") if ($ep | is-empty) { error make --unspanned { msg: $"[push-registry-env-unset] env var ($v) not set" } } { endpoint: $ep, username: ($env | get -o (cfg "registry.push.user_env" "PROVISIONING_REGISTRY_USER") | default ""), password: ($env | get -o (cfg "registry.push.pass_env" "PROVISIONING_REGISTRY_PASS") | default ""), } }, "sops" => (resolve-from-sops $v $endpoint_key $user_key $pass_key $vault_id $age_key), "api" => (resolve-from-api $v $endpoint_key $user_key $pass_key $auth_header), _ => { error make --unspanned { msg: $"[push-registry-kind-unknown] ($k)" } }, } } # Resolve the push registry and emit shell `export` lines materializing it as an # env setting. Capture with: eval "$(... select-push-registry --kind sops ...)" # Sets PROVISIONING_REGISTRY (endpoint) and, when credentials are present, # PROVISIONING_PUSH_DOCKER_CONFIG (an isolated docker config dir the OCI resolver # uses instead of the workspace-derived credential). export def select-push-registry [ --kind: string = "" --value: string = "" --endpoint-key: string = "endpoint" --user-key: string = "username" --pass-key: string = "password" --vault-id: string = "" --age-key: string = "" --auth-header: string = "" ]: nothing -> nothing { let r = (resolve-push-registry --kind $kind --value $value --endpoint-key $endpoint_key --user-key $user_key --pass-key $pass_key --vault-id $vault_id --age-key $age_key --auth-header $auth_header) if ($r.endpoint | is-empty) { error make --unspanned { msg: "push-registry resolved to an empty endpoint" } } print $"export PROVISIONING_REGISTRY=($r.endpoint)" if ($r.username | is-not-empty) { let dir = (build-docker-config-tmpdir $r.endpoint $r.username $r.password) print $"export PROVISIONING_PUSH_DOCKER_CONFIG=($dir)" } }