#!/usr/bin/env nu # Derived variables for cert_manager bundle rendering. # cf_api_token is injected by components.nu via SOPS decryption before this script runs. # CF_TOKEN_ env var still works as an override (CI/CD pipelines). # Renders per-issuer blocks into flat strings for {{var}} substitution. def render_solver [s: record]: nothing -> string { # selector must nest INSIDE this solver's own list item, as a sibling of # dns01 (cert-manager's Solver type) — not a sibling of the "solvers" # array, which YAML would otherwise happily accept with no warning while # cert-manager silently misapplies it. Built as an explicit line list so # indentation is exact regardless of which fields are present. let dns_zones = ($s.dns_zones? | default []) let lines = ( if ($dns_zones | is-not-empty) { let zone_lines = ($dns_zones | each {|z| $" - \"($z)\"" }) [" - selector:", " dnsZones:"] ++ $zone_lines ++ [" dns01:"] } else { [" - dns01:"] } ) ++ [ " cloudflare:", " apiTokenSecretRef:", $" name: ($s.secret_ref)", " key: api-token", ] $lines | str join "\n" } # One issuer can carry multiple DNS-01 solvers — a catch-all (no dns_zones) # plus zone-scoped ones for certs whose SANs span more than one Cloudflare # zone. issuer.solvers takes precedence; issuer.secret_ref (single, legacy # shape) is used only when solvers is absent, for backward compatibility. def render_acme_issuer [issuer: record]: nothing -> string { let server = ($issuer.acme_server? | default "https://acme-v02.api.letsencrypt.org/directory") let email = ($issuer.email? | default "") let solvers = if ($issuer.solvers? | default [] | is-not-empty) { $issuer.solvers } else { [{ secret_ref: ($issuer.secret_ref? | default "") }] } let solvers_block = ($solvers | each {|s| render_solver $s } | str join "\n") $"apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: ($issuer.name) spec: acme: server: ($server) email: ($email) privateKeySecretRef: name: ($issuer.name)-acme-key solvers: ($solvers_block)" } def cf_token_from_env [secret_ref: string]: nothing -> string { let env_var = ($"CF_TOKEN_($secret_ref | str upcase | str replace --all '-' '_')") $env | get -o $env_var | default "" } def main [component_json: string]: nothing -> nothing { let d = ($component_json | from json) let ns = ($d.namespace? | default "cert-manager") let version = ($d.version? | default "v1.19.5") let issuers_block = ( $d.cluster_issuers? | default [] | where { |i| ($i.kind? | default "") == "acme" } | each { |i| render_acme_issuer $i } | str join "\n---\n" ) let acme_issuers = ( $d.cluster_issuers? | default [] | where { |i| ($i.kind? | default "") == "acme" } ) let cf_secret_ref = ( if ($acme_issuers | length) > 0 { $acme_issuers | first | get -o secret_ref | default "" } else { "" } ) let injected = ($d.cf_api_token? | default "") let cf_token = if ($injected | is-not-empty) { $injected } else { cf_token_from_env $cf_secret_ref } let tls_backup = ($d.tls_backup? | default {}) { namespace: $ns, version: $version, issuers_block: $issuers_block, cf_secret_ref: $cf_secret_ref, cf_api_token: $cf_token, restic_repository: ($env.RESTIC_REPOSITORY? | default ""), restic_password: ($env.RESTIC_PASSWORD? | default ""), aws_access_key_id: ($env.AWS_ACCESS_KEY_ID? | default ""), aws_secret_access_key: ($env.AWS_SECRET_ACCESS_KEY? | default ""), aws_default_region: ($env.AWS_DEFAULT_REGION? | default ""), tls_backup_enabled: ($tls_backup.enabled? | default false), tls_backup_kubectl_version: ($tls_backup.kubectl_version? | default "v1.32.3"), tls_backup_nushell_image: ($tls_backup.nushell_image? | default "ghcr.io/nushell/nushell:latest"), } | to json --raw | print }