provisioning-core/nulib/platform/oci/push_registry.nu

134 lines
6.2 KiB
Text
Raw Permalink Normal View History

#!/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)"
}
}