From 1f6799271a1187da6c846476c1a41a1ac551e4a0 Mon Sep 17 00:00:00 2001 From: juvdiaz Date: Fri, 29 May 2026 12:23:44 -0600 Subject: [PATCH] Adding local tags to images and enforcing them at pods --- README.md | 16 ++++++-- apps/website/kustomization.yaml | 5 +++ apps/website/lang/en.php | 2 +- apps/website/lang/nah.php | 2 +- apps/website/web-app.yaml | 4 +- bootstrap/apps/main.tf | 23 ++++++++--- bootstrap/apps/variables.tf | 5 +++ lab.sh | 71 ++++++++++++++++++++++++++++----- 8 files changed, 104 insertions(+), 24 deletions(-) create mode 100644 apps/website/kustomization.yaml diff --git a/README.md b/README.md index 28c6d27..b940229 100644 --- a/README.md +++ b/README.md @@ -528,9 +528,17 @@ them through the edge path at `/demo-apps/`. `./lab.sh up` builds and pushes two independent images: -- `php-website:latest` from `apps/website` +- a content-hash `php-website` tag generated by `lab.sh` and passed to Argo CD + as a Kustomize image override - `demos-static:latest` from `apps/demos-static` +The website manifest keeps the stable base image name `php-website:bootstrap`. +During bootstrap, `lab.sh` hashes `apps/website`, builds +`/php-website:src-`, exports that exact reference through +`TF_VAR_website_image_ref`, and the Argo CD Application applies it through +Kustomize. This keeps the GitOps source generic while the deployed image remains +immutable. + The first demo, `The Client-Side Media Cruncher (Wasm + TS)`, currently performs private, browser-only image compression and conversion using native Canvas APIs. Heavier video conversion, such as MP4 to WebM, should use a Rust core compiled @@ -566,9 +574,9 @@ Current demo inventory: - Model drift simulator: visual MLOps playground for spikes, corrupted inputs, and retraining. -The Kubernetes deployment uses `apps/website/web-app.yaml`. Keep the image -reference there aligned with `TF_VAR_registry_endpoint`, because `lab.sh` derives -the registry endpoint from that manifest. +The Kubernetes deployment uses `apps/website/web-app.yaml` as a Kustomize base. +Keep `TF_VAR_registry_endpoint` aligned with the local registry endpoint used by +the app image build. Keep the `.terraform.lock.hcl` files committed. They pin provider selections and make bootstrap behavior reproducible across nodes and rebuilds. diff --git a/apps/website/kustomization.yaml b/apps/website/kustomization.yaml new file mode 100644 index 0000000..9cb3a02 --- /dev/null +++ b/apps/website/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - namespace.yaml + - web-app.yaml diff --git a/apps/website/lang/en.php b/apps/website/lang/en.php index 40fb995..5f24bfb 100644 --- a/apps/website/lang/en.php +++ b/apps/website/lang/en.php @@ -175,7 +175,7 @@ return [ 'blog_todo_1' => 'Move Gitea data from the Raspberry Pi SD card to SSD-backed storage.', 'blog_todo_2' => 'Keep the Debian bare GitOps mirror as the cluster source and add object-storage backups when OCI storage is ready.', 'blog_todo_3' => 'Add a real OpenTofu remote state backend with backup, locking, and a documented recovery path.', - 'blog_todo_4' => 'Replace mutable latest image references with immutable tags or digest pins for website and demo workloads.', + 'blog_todo_4' => 'Replace the remaining mutable latest image references with immutable tags or digest pins for demo workloads; the website image now uses a content-hash tag.', 'blog_todo_5' => 'Generate SBOMs and sign images so the local registry can prove what it is serving.', 'blog_todo_6' => 'Add Renovate or Dependabot-style dependency updates for base images, Helm charts, and GitHub/Gitea Actions.', 'blog_todo_7' => 'Expand Kyverno baseline policy coverage: non-root, read-only roots, resource requests, allowed registries, and documented exceptions for platform components.', diff --git a/apps/website/lang/nah.php b/apps/website/lang/nah.php index 3512ec7..9c384a3 100644 --- a/apps/website/lang/nah.php +++ b/apps/website/lang/nah.php @@ -175,7 +175,7 @@ return [ 'blog_todo_1' => 'Move Gitea data from Raspberry Pi SD card to SSD-backed storage.', 'blog_todo_2' => 'Keep Debian bare GitOps mirror as cluster source ihuan add object-storage backups quema OCI storage ready.', 'blog_todo_3' => 'Add real OpenTofu remote state backend ika backup, locking, ihuan documented recovery path.', - 'blog_todo_4' => 'Replace mutable latest image references ika immutable tags o digest pins para website ihuan demo workloads.', + 'blog_todo_4' => 'Replace remaining mutable latest image references ika immutable tags o digest pins para demo workloads; website image axcan uses content-hash tag.', 'blog_todo_5' => 'Generate SBOMs ihuan sign images so local registry can prove tlein serving.', 'blog_todo_6' => 'Add Renovate o Dependabot-style dependency updates para base images, Helm charts, ihuan GitHub/Gitea Actions.', 'blog_todo_7' => 'Expand Kyverno baseline policy coverage: non-root, read-only roots, resource requests, allowed registries, ihuan documented exceptions para platform components.', diff --git a/apps/website/web-app.yaml b/apps/website/web-app.yaml index 7419d7e..d62eb93 100644 --- a/apps/website/web-app.yaml +++ b/apps/website/web-app.yaml @@ -55,8 +55,8 @@ spec: topologyKey: "kubernetes.io/hostname" containers: - name: php-app - image: 192.168.100.68:30500/php-website:latest - imagePullPolicy: Always + image: php-website:bootstrap + imagePullPolicy: IfNotPresent securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true diff --git a/bootstrap/apps/main.tf b/bootstrap/apps/main.tf index d20764a..b760c8e 100644 --- a/bootstrap/apps/main.tf +++ b/bootstrap/apps/main.tf @@ -12,6 +12,23 @@ provider "kubernetes" { config_path = var.kubeconfig_path } +locals { + application_sources = { + for name, application in var.applications : name => merge( + { + repoURL = var.gitops_repo_url + targetRevision = application.target_revision + path = application.path + }, + name == "website-production" && var.website_image_ref != "" ? { + kustomize = { + images = ["php-website=${var.website_image_ref}"] + } + } : {} + ) + } +} + resource "kubernetes_manifest" "argocd_application" { for_each = var.applications @@ -28,11 +45,7 @@ resource "kubernetes_manifest" "argocd_application" { } spec = { project = each.value.project - source = { - repoURL = var.gitops_repo_url - targetRevision = each.value.target_revision - path = each.value.path - } + source = local.application_sources[each.key] destination = { server = "https://kubernetes.default.svc" namespace = each.value.namespace diff --git a/bootstrap/apps/variables.tf b/bootstrap/apps/variables.tf index a8a2ab4..561a59d 100644 --- a/bootstrap/apps/variables.tf +++ b/bootstrap/apps/variables.tf @@ -13,6 +13,11 @@ variable "gitops_repo_url" { default = "ssh://jv@192.168.100.68/home/jv/git-server/my-homelab-configs.git" } +variable "website_image_ref" { + type = string + default = "" +} + variable "applications" { type = map(object({ project = string diff --git a/lab.sh b/lab.sh index 8d4c566..277706e 100755 --- a/lab.sh +++ b/lab.sh @@ -1363,16 +1363,32 @@ cleanup_node() { sudo systemctl start containerd 2>/dev/null || true } -website_registry_endpoint() { - local image +image_ref_tag() { + local image_ref="$1" + local tag - image="$(awk '$1 == "image:" && $2 ~ /php-website/ {print $2; exit}' "${REPO_ROOT}/apps/website/web-app.yaml")" - if [[ -z "${image}" || "${image}" != */* ]]; then - echo "Could not determine website registry endpoint from apps/website/web-app.yaml" >&2 + tag="${image_ref##*:}" + if [[ "${tag}" == "${image_ref}" || "${tag}" == */* ]]; then + echo "Image reference ${image_ref} must include an immutable tag." >&2 exit 1 fi - printf '%s\n' "${image%%/*}" + printf '%s\n' "${tag}" +} + +website_image_tag() { + local source_hash="$1" + + printf 'src-%s\n' "${source_hash:0:12}" +} + +apps_registry_endpoint() { + if [[ -n "${TF_VAR_registry_endpoint:-}" ]]; then + printf '%s\n' "${TF_VAR_registry_endpoint}" + return 0 + fi + + demos_registry_endpoint } demos_registry_endpoint() { @@ -1430,6 +1446,7 @@ website_image_is_current() { local platforms="$3" local image_ref="$4" local registry_endpoint="$5" + local image_tag local saved_hash local saved_platforms local saved_image @@ -1444,7 +1461,27 @@ website_image_is_current() { [[ "${saved_platforms}" == "${platforms}" ]] || return 1 [[ "${saved_image}" == "${image_ref}" ]] || return 1 - registry_image_exists "${registry_endpoint}" php-website latest + image_tag="$(image_ref_tag "${image_ref}")" + registry_image_exists "${registry_endpoint}" php-website "${image_tag}" +} + +ensure_website_image_tag_not_reused() { + local state_file="$1" + local source_hash="$2" + local image_ref="$3" + local saved_hash + local saved_image + + [[ -f "${state_file}" ]] || return 0 + + saved_hash="$(image_state_value "${state_file}" source_hash)" + saved_image="$(image_state_value "${state_file}" image)" + + if [[ -n "${saved_hash}" && -n "${saved_image}" && "${saved_hash}" != "${source_hash}" && "${saved_image}" == "${image_ref}" ]]; then + echo "Website source changed but the computed php-website image ref is still ${image_ref}." >&2 + echo "Check WEBSITE_IMAGE_TAG or the website image tag generator before rebuilding." >&2 + exit 1 + fi } demos_image_is_current() { @@ -2533,30 +2570,42 @@ apps() { require_debian_server "apps" - registry_endpoint="$(website_registry_endpoint)" + registry_endpoint="$(apps_registry_endpoint)" demos_registry_endpoint="$(demos_registry_endpoint)" demos_image_ref="${registry_endpoint}/demos-static:latest" demos_image_state_file="${REPO_ROOT}/.lab/demos-static-image.state" demos_platforms="${DEMOS_IMAGE_PLATFORMS:-linux/arm64}" demos_source_hash="$(demos_source_hash)" - website_image_ref="${registry_endpoint}/php-website:latest" website_image_state_file="${REPO_ROOT}/.lab/php-website-image.state" website_platforms="${WEBSITE_IMAGE_PLATFORMS:-linux/arm64}" website_source_hash="$(website_source_hash)" + website_image_ref="${registry_endpoint}/php-website:${WEBSITE_IMAGE_TAG:-$(website_image_tag "${website_source_hash}")}" export TF_VAR_registry_endpoint="${TF_VAR_registry_endpoint:-${registry_endpoint}}" + export TF_VAR_website_image_ref="${TF_VAR_website_image_ref:-${website_image_ref}}" export TF_VAR_kubeconfig_path="${TF_VAR_kubeconfig_path:-${KUBECONFIG_PATH}}" export KUBECONFIG="${TF_VAR_kubeconfig_path}" if [[ "${TF_VAR_registry_endpoint}" != "${registry_endpoint}" ]]; then - echo "TF_VAR_registry_endpoint must match apps/website/web-app.yaml (${registry_endpoint})" >&2 + echo "TF_VAR_registry_endpoint changed after registry endpoint resolution (${registry_endpoint})" >&2 + exit 1 + fi + + if [[ "${TF_VAR_website_image_ref}" != "${website_image_ref}" ]]; then + echo "TF_VAR_website_image_ref must match the buildx website image ref (${website_image_ref})" >&2 exit 1 fi if [[ "${demos_registry_endpoint}" != "${registry_endpoint}" ]]; then - echo "apps/demos-static/web-app.yaml registry endpoint (${demos_registry_endpoint}) must match apps/website/web-app.yaml (${registry_endpoint})" >&2 + echo "apps/demos-static/web-app.yaml registry endpoint (${demos_registry_endpoint}) must match app registry endpoint (${registry_endpoint})" >&2 exit 1 fi + if [[ "$(image_ref_tag "${website_image_ref}")" == "latest" ]]; then + echo "apps/website/web-app.yaml must use an immutable php-website image tag, not latest." >&2 + exit 1 + fi + ensure_website_image_tag_not_reused "${website_image_state_file}" "${website_source_hash}" "${website_image_ref}" + echo "Deploying homelab applications..." run_tofu_stack "bootstrap/apps"