Adding local tags to images and enforcing them at pods
Homelab Main / deploy (push) Successful in 1m21s
Details
Homelab Main / deploy (push) Successful in 1m21s
Details
This commit is contained in:
parent
6e78988a6a
commit
1f6799271a
16
README.md
16
README.md
|
|
@ -528,9 +528,17 @@ them through the edge path at `/demo-apps/`.
|
||||||
|
|
||||||
`./lab.sh up` builds and pushes two independent images:
|
`./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`
|
- `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
|
||||||
|
`<registry>/php-website:src-<hash>`, 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
|
The first demo, `The Client-Side Media Cruncher (Wasm + TS)`, currently performs
|
||||||
private, browser-only image compression and conversion using native Canvas APIs.
|
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
|
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,
|
- Model drift simulator: visual MLOps playground for spikes, corrupted inputs,
|
||||||
and retraining.
|
and retraining.
|
||||||
|
|
||||||
The Kubernetes deployment uses `apps/website/web-app.yaml`. Keep the image
|
The Kubernetes deployment uses `apps/website/web-app.yaml` as a Kustomize base.
|
||||||
reference there aligned with `TF_VAR_registry_endpoint`, because `lab.sh` derives
|
Keep `TF_VAR_registry_endpoint` aligned with the local registry endpoint used by
|
||||||
the registry endpoint from that manifest.
|
the app image build.
|
||||||
|
|
||||||
Keep the `.terraform.lock.hcl` files committed. They pin provider selections and
|
Keep the `.terraform.lock.hcl` files committed. They pin provider selections and
|
||||||
make bootstrap behavior reproducible across nodes and rebuilds.
|
make bootstrap behavior reproducible across nodes and rebuilds.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
resources:
|
||||||
|
- namespace.yaml
|
||||||
|
- web-app.yaml
|
||||||
|
|
@ -175,7 +175,7 @@ return [
|
||||||
'blog_todo_1' => 'Move Gitea data from the Raspberry Pi SD card to SSD-backed storage.',
|
'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_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_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_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_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.',
|
'blog_todo_7' => 'Expand Kyverno baseline policy coverage: non-root, read-only roots, resource requests, allowed registries, and documented exceptions for platform components.',
|
||||||
|
|
|
||||||
|
|
@ -175,7 +175,7 @@ return [
|
||||||
'blog_todo_1' => 'Move Gitea data from Raspberry Pi SD card to SSD-backed storage.',
|
'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_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_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_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_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.',
|
'blog_todo_7' => 'Expand Kyverno baseline policy coverage: non-root, read-only roots, resource requests, allowed registries, ihuan documented exceptions para platform components.',
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,8 @@ spec:
|
||||||
topologyKey: "kubernetes.io/hostname"
|
topologyKey: "kubernetes.io/hostname"
|
||||||
containers:
|
containers:
|
||||||
- name: php-app
|
- name: php-app
|
||||||
image: 192.168.100.68:30500/php-website:latest
|
image: php-website:bootstrap
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: IfNotPresent
|
||||||
securityContext:
|
securityContext:
|
||||||
allowPrivilegeEscalation: false
|
allowPrivilegeEscalation: false
|
||||||
readOnlyRootFilesystem: true
|
readOnlyRootFilesystem: true
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,23 @@ provider "kubernetes" {
|
||||||
config_path = var.kubeconfig_path
|
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" {
|
resource "kubernetes_manifest" "argocd_application" {
|
||||||
for_each = var.applications
|
for_each = var.applications
|
||||||
|
|
||||||
|
|
@ -28,11 +45,7 @@ resource "kubernetes_manifest" "argocd_application" {
|
||||||
}
|
}
|
||||||
spec = {
|
spec = {
|
||||||
project = each.value.project
|
project = each.value.project
|
||||||
source = {
|
source = local.application_sources[each.key]
|
||||||
repoURL = var.gitops_repo_url
|
|
||||||
targetRevision = each.value.target_revision
|
|
||||||
path = each.value.path
|
|
||||||
}
|
|
||||||
destination = {
|
destination = {
|
||||||
server = "https://kubernetes.default.svc"
|
server = "https://kubernetes.default.svc"
|
||||||
namespace = each.value.namespace
|
namespace = each.value.namespace
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,11 @@ variable "gitops_repo_url" {
|
||||||
default = "ssh://jv@192.168.100.68/home/jv/git-server/my-homelab-configs.git"
|
default = "ssh://jv@192.168.100.68/home/jv/git-server/my-homelab-configs.git"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variable "website_image_ref" {
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|
||||||
variable "applications" {
|
variable "applications" {
|
||||||
type = map(object({
|
type = map(object({
|
||||||
project = string
|
project = string
|
||||||
|
|
|
||||||
71
lab.sh
71
lab.sh
|
|
@ -1363,16 +1363,32 @@ cleanup_node() {
|
||||||
sudo systemctl start containerd 2>/dev/null || true
|
sudo systemctl start containerd 2>/dev/null || true
|
||||||
}
|
}
|
||||||
|
|
||||||
website_registry_endpoint() {
|
image_ref_tag() {
|
||||||
local image
|
local image_ref="$1"
|
||||||
|
local tag
|
||||||
|
|
||||||
image="$(awk '$1 == "image:" && $2 ~ /php-website/ {print $2; exit}' "${REPO_ROOT}/apps/website/web-app.yaml")"
|
tag="${image_ref##*:}"
|
||||||
if [[ -z "${image}" || "${image}" != */* ]]; then
|
if [[ "${tag}" == "${image_ref}" || "${tag}" == */* ]]; then
|
||||||
echo "Could not determine website registry endpoint from apps/website/web-app.yaml" >&2
|
echo "Image reference ${image_ref} must include an immutable tag." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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() {
|
demos_registry_endpoint() {
|
||||||
|
|
@ -1430,6 +1446,7 @@ website_image_is_current() {
|
||||||
local platforms="$3"
|
local platforms="$3"
|
||||||
local image_ref="$4"
|
local image_ref="$4"
|
||||||
local registry_endpoint="$5"
|
local registry_endpoint="$5"
|
||||||
|
local image_tag
|
||||||
local saved_hash
|
local saved_hash
|
||||||
local saved_platforms
|
local saved_platforms
|
||||||
local saved_image
|
local saved_image
|
||||||
|
|
@ -1444,7 +1461,27 @@ website_image_is_current() {
|
||||||
[[ "${saved_platforms}" == "${platforms}" ]] || return 1
|
[[ "${saved_platforms}" == "${platforms}" ]] || return 1
|
||||||
[[ "${saved_image}" == "${image_ref}" ]] || 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() {
|
demos_image_is_current() {
|
||||||
|
|
@ -2533,30 +2570,42 @@ apps() {
|
||||||
|
|
||||||
require_debian_server "apps"
|
require_debian_server "apps"
|
||||||
|
|
||||||
registry_endpoint="$(website_registry_endpoint)"
|
registry_endpoint="$(apps_registry_endpoint)"
|
||||||
demos_registry_endpoint="$(demos_registry_endpoint)"
|
demos_registry_endpoint="$(demos_registry_endpoint)"
|
||||||
demos_image_ref="${registry_endpoint}/demos-static:latest"
|
demos_image_ref="${registry_endpoint}/demos-static:latest"
|
||||||
demos_image_state_file="${REPO_ROOT}/.lab/demos-static-image.state"
|
demos_image_state_file="${REPO_ROOT}/.lab/demos-static-image.state"
|
||||||
demos_platforms="${DEMOS_IMAGE_PLATFORMS:-linux/arm64}"
|
demos_platforms="${DEMOS_IMAGE_PLATFORMS:-linux/arm64}"
|
||||||
demos_source_hash="$(demos_source_hash)"
|
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_image_state_file="${REPO_ROOT}/.lab/php-website-image.state"
|
||||||
website_platforms="${WEBSITE_IMAGE_PLATFORMS:-linux/arm64}"
|
website_platforms="${WEBSITE_IMAGE_PLATFORMS:-linux/arm64}"
|
||||||
website_source_hash="$(website_source_hash)"
|
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_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 TF_VAR_kubeconfig_path="${TF_VAR_kubeconfig_path:-${KUBECONFIG_PATH}}"
|
||||||
export KUBECONFIG="${TF_VAR_kubeconfig_path}"
|
export KUBECONFIG="${TF_VAR_kubeconfig_path}"
|
||||||
|
|
||||||
if [[ "${TF_VAR_registry_endpoint}" != "${registry_endpoint}" ]]; then
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "${demos_registry_endpoint}" != "${registry_endpoint}" ]]; then
|
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
|
exit 1
|
||||||
fi
|
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..."
|
echo "Deploying homelab applications..."
|
||||||
|
|
||||||
run_tofu_stack "bootstrap/apps"
|
run_tofu_stack "bootstrap/apps"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue