Anca's plotting-book app now builds its image in her own GitHub repo to
the private package ghcr.io/passionprojectsanca/book-plotter (off public
DockerHub viktorbarzin/book-plotter). Wire the cluster to pull it:
- stacks/plotting-book: point the deployment baseline image at the ghcr
package and add imagePullSecrets {ghcr-credentials} so the pod can pull
the private image (the live tag is still CI-owned via ignore_changes).
- stacks/kyverno: add the plotting-book namespace to the ghcr-credentials
allowlist so the Kyverno generate policy clones the pull secret into it.
Verified the shared ghcr_pull_token (Viktor, repo-admin on Anca's repo)
can read the private package before wiring this.
Docs: correct ci-cd.md (it wrongly listed plotting-book as already on
ghcr — it was on DockerHub) and note the special arrangement; amend
ADR-0003 to record that this GitHub-first repo builds to its own org's
ghcr namespace.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
112 lines
4.1 KiB
HCL
112 lines
4.1 KiB
HCL
# =============================================================================
|
|
# ghcr.io pull credentials — synced ONLY to namespaces running PRIVATE ghcr
|
|
# images (ADR-0002 off-infra builds)
|
|
# =============================================================================
|
|
# The credential is Viktor's admin PAT (Vault secret/viktor/ghcr_pull_token —
|
|
# an alias of github_pat: GitHub has no API to mint tokens, so a UI-minted
|
|
# read:packages token can replace the alias value later with no TF change).
|
|
# Because the PAT is broad, this is a positive allowlist, NOT cluster-wide
|
|
# like registry-credentials: any workload in a listed namespace can read the
|
|
# secret, so every entry widens the blast radius. Public-image namespaces
|
|
# need no credentials — keep this list to private-image consumers only.
|
|
|
|
locals {
|
|
ghcr_private_namespaces = [
|
|
"tripit",
|
|
# tuya-bridge runs a PUBLIC-decision image, but new ghcr packages default
|
|
# PRIVATE until their visibility is flipped (UI) — safety net so pulls
|
|
# work from the first deploy; prune once the package is public.
|
|
"tuya-bridge",
|
|
"f1-stream",
|
|
"job-hunter",
|
|
"instagram-poster",
|
|
"payslip-ingest",
|
|
"wealthfolio",
|
|
"fire-planner",
|
|
"recruiter-responder",
|
|
# openclaw's install-recruiter-plugin init container pulls the PRIVATE
|
|
# ghcr.io/viktorbarzin/recruiter-responder:latest image (infra#27).
|
|
"openclaw",
|
|
# k8s-portal: last in-cluster image build, migrated to GHA→ghcr (ADR-0002,
|
|
# "no local builds"). ghcr.io/viktorbarzin/k8s-portal:latest is PRIVATE
|
|
# (infra repo default); the deployment references the cloned secret.
|
|
"k8s-portal",
|
|
# goldmane-edge-aggregator: PRIVATE ghcr image pulled by the aggregate
|
|
# Deployment + digest CronJob (ADR-0014, infra#58).
|
|
"goldmane-edge-aggregator",
|
|
# plotting-book: image migrated from public DockerHub to PRIVATE
|
|
# ghcr.io/passionprojectsanca/book-plotter (built by GHA in Anca's repo,
|
|
# under her own org's ghcr). The deployment references the cloned secret.
|
|
"plotting-book",
|
|
]
|
|
}
|
|
|
|
resource "kubernetes_secret" "ghcr_credentials" {
|
|
metadata {
|
|
name = "ghcr-credentials"
|
|
namespace = kubernetes_namespace.kyverno.metadata[0].name
|
|
}
|
|
type = "kubernetes.io/dockerconfigjson"
|
|
data = {
|
|
".dockerconfigjson" = jsonencode({
|
|
auths = {
|
|
"ghcr.io" = {
|
|
username = "ViktorBarzin"
|
|
password = try(data.vault_kv_secret_v2.viktor.data["ghcr_pull_token"], "")
|
|
auth = base64encode("ViktorBarzin:${try(data.vault_kv_secret_v2.viktor.data["ghcr_pull_token"], "")}")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
resource "kubectl_manifest" "sync_ghcr_credentials" {
|
|
# Kyverno's validate-policy webhook DENIES in-place changes to a generate
|
|
# rule's spec ("changes of immutable fields ... is disallowed"), so any
|
|
# allowlist edit must delete+recreate the policy. Generated secrets survive
|
|
# policy deletion; generateExisting re-adopts them on recreate.
|
|
force_new = true
|
|
yaml_body = yamlencode({
|
|
apiVersion = "kyverno.io/v1"
|
|
kind = "ClusterPolicy"
|
|
metadata = {
|
|
name = "sync-ghcr-credentials"
|
|
}
|
|
spec = {
|
|
rules = [
|
|
{
|
|
name = "sync-ghcr-secret"
|
|
match = {
|
|
any = [
|
|
{
|
|
resources = {
|
|
kinds = ["Namespace"]
|
|
names = local.ghcr_private_namespaces
|
|
}
|
|
}
|
|
]
|
|
}
|
|
generate = {
|
|
generateExisting = true
|
|
apiVersion = "v1"
|
|
kind = "Secret"
|
|
name = "ghcr-credentials"
|
|
namespace = "{{request.object.metadata.name}}"
|
|
synchronize = true
|
|
clone = {
|
|
namespace = "kyverno"
|
|
name = "ghcr-credentials"
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
})
|
|
|
|
depends_on = [
|
|
helm_release.kyverno,
|
|
kubernetes_secret.ghcr_credentials,
|
|
kubernetes_cluster_role_binding.kyverno_admission_secret_manager,
|
|
kubernetes_cluster_role_binding.kyverno_background_secret_manager,
|
|
]
|
|
}
|