Fix tf-state-backup CronJob — replace dead bitnami/kubectl image #52

Merged
forgejo_admin merged 1 commit from 51-fix-tf-state-backup-cronjob-replace-dead into main 2026-03-14 18:25:52 +00:00

Summary

Bitnami removed all Docker Hub images, breaking the nightly tf-state-backup CronJob. This PR switches to alpine:3.20 and downloads the kubectl binary at runtime, matching the existing pattern used for the MinIO client.

Changes

  • terraform/main.tf — CronJob container spec:
    • Image: bitnami/kubectl:1.31alpine:3.20
    • Shell: /bin/bash/bin/sh
    • Added apk add --no-cache curl (alpine does not ship curl)
    • Added kubectl v1.31.4 binary download to /tmp/kubectl (same pattern as mc client)
    • Replaced bare kubectl with /tmp/kubectl in the backup loop

Test Plan

  • tofu fmt — passed (no changes)
  • tofu validate — passed
  • After merge and apply, verify the CronJob runs successfully: kubectl create job --from=cronjob/tf-state-backup tf-state-backup-test -n tofu-state then check logs

Review Checklist

  • tofu fmt passed
  • tofu validate passed
  • Only terraform/main.tf modified
  • Existing mc download pattern preserved
  • Image pinned to alpine:3.20 (no :latest)
  • Manual CronJob trigger after apply to verify end-to-end
  • Plan: plan-pal-e-platform
  • Forgejo issue: #51

Closes #51

## Summary Bitnami removed all Docker Hub images, breaking the nightly tf-state-backup CronJob. This PR switches to `alpine:3.20` and downloads the kubectl binary at runtime, matching the existing pattern used for the MinIO client. ## Changes - **terraform/main.tf** — CronJob container spec: - Image: `bitnami/kubectl:1.31` → `alpine:3.20` - Shell: `/bin/bash` → `/bin/sh` - Added `apk add --no-cache curl` (alpine does not ship curl) - Added kubectl v1.31.4 binary download to `/tmp/kubectl` (same pattern as mc client) - Replaced bare `kubectl` with `/tmp/kubectl` in the backup loop ## Test Plan - `tofu fmt` — passed (no changes) - `tofu validate` — passed - After merge and apply, verify the CronJob runs successfully: `kubectl create job --from=cronjob/tf-state-backup tf-state-backup-test -n tofu-state` then check logs ## Review Checklist - [x] `tofu fmt` passed - [x] `tofu validate` passed - [x] Only `terraform/main.tf` modified - [x] Existing mc download pattern preserved - [x] Image pinned to `alpine:3.20` (no :latest) - [ ] Manual CronJob trigger after apply to verify end-to-end ## Related - Plan: `plan-pal-e-platform` - Forgejo issue: #51 Closes #51
Fix tf-state-backup CronJob: replace dead bitnami/kubectl with alpine
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
451cb0f54a
Bitnami removed all Docker Hub images, breaking the nightly backup
CronJob. Switch to alpine:3.20 with curl installed via apk, download
kubectl binary to /tmp at runtime (same pattern as the mc client),
and use /bin/sh instead of /bin/bash.

Closes #51

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author
Owner

Self-Review

Result: No issues found.

Verified:

  • alpine:3.20 pinned (no :latest)
  • /bin/sh replaces /bin/bash (alpine uses busybox ash, no bash available)
  • apk add --no-cache curl installs curl before any curl usage
  • kubectl v1.31.4 binary downloaded to /tmp/kubectl — same pattern as the mc client
  • Only kubectl invocation in the script updated to /tmp/kubectl
  • set -euo pipefail works under busybox ash on alpine
  • tofu fmt and tofu validate both pass
  • Single file changed, +10/-3 lines
## Self-Review **Result: No issues found.** Verified: - `alpine:3.20` pinned (no `:latest`) - `/bin/sh` replaces `/bin/bash` (alpine uses busybox ash, no bash available) - `apk add --no-cache curl` installs curl before any curl usage - kubectl v1.31.4 binary downloaded to `/tmp/kubectl` — same pattern as the mc client - Only `kubectl` invocation in the script updated to `/tmp/kubectl` - `set -euo pipefail` works under busybox ash on alpine - `tofu fmt` and `tofu validate` both pass - Single file changed, +10/-3 lines
Author
Owner

Tofu Plan Output

tailscale_acl.this: Refreshing state... [id=acl]
helm_release.nvidia_device_plugin: Refreshing state... [id=nvidia-device-plugin]
kubernetes_namespace_v1.monitoring: Refreshing state... [id=monitoring]
kubernetes_namespace_v1.woodpecker: Refreshing state... [id=woodpecker]
kubernetes_namespace_v1.keycloak: Refreshing state... [id=keycloak]
kubernetes_namespace_v1.ollama: Refreshing state... [id=ollama]
kubernetes_namespace_v1.harbor: Refreshing state... [id=harbor]
kubernetes_namespace_v1.forgejo: Refreshing state... [id=forgejo]
kubernetes_namespace_v1.minio: Refreshing state... [id=minio]
kubernetes_namespace_v1.postgres: Refreshing state... [id=postgres]
kubernetes_namespace_v1.tailscale: Refreshing state... [id=tailscale]
data.kubernetes_namespace_v1.pal_e_docs: Reading...
data.kubernetes_namespace_v1.tofu_state: Reading...
kubernetes_persistent_volume_claim_v1.keycloak_data: Refreshing state... [id=keycloak/keycloak-data]
kubernetes_service_v1.keycloak: Refreshing state... [id=keycloak/keycloak]
kubernetes_secret_v1.keycloak_admin: Refreshing state... [id=keycloak/keycloak-admin]
kubernetes_namespace_v1.cnpg_system: Refreshing state... [id=cnpg-system]
helm_release.forgejo: Refreshing state... [id=forgejo]
data.kubernetes_namespace_v1.pal_e_docs: Read complete after 0s [id=pal-e-docs]
data.kubernetes_namespace_v1.tofu_state: Read complete after 0s [id=tofu-state]
kubernetes_service_v1.dora_exporter: Refreshing state... [id=monitoring/dora-exporter]
helm_release.kube_prometheus_stack: Refreshing state... [id=kube-prometheus-stack]
helm_release.loki_stack: Refreshing state... [id=loki-stack]
kubernetes_secret_v1.dora_exporter: Refreshing state... [id=monitoring/dora-exporter]
kubernetes_secret_v1.paledocs_db_url: Refreshing state... [id=pal-e-docs/paledocs-db-url]
kubernetes_service_account_v1.tf_backup: Refreshing state... [id=tofu-state/tf-state-backup]
kubernetes_role_v1.tf_backup: Refreshing state... [id=tofu-state/tf-state-backup]
helm_release.cnpg: Refreshing state... [id=cnpg]
helm_release.tailscale_operator: Refreshing state... [id=tailscale-operator]
kubernetes_role_binding_v1.tf_backup: Refreshing state... [id=tofu-state/tf-state-backup]
kubernetes_deployment_v1.keycloak: Refreshing state... [id=keycloak/keycloak]
helm_release.ollama: Refreshing state... [id=ollama]
helm_release.minio: Refreshing state... [id=minio]
kubernetes_config_map_v1.dora_dashboard: Refreshing state... [id=monitoring/dora-dashboard]
helm_release.harbor: Refreshing state... [id=harbor]
kubernetes_deployment_v1.dora_exporter: Refreshing state... [id=monitoring/dora-exporter]
kubernetes_manifest.dora_exporter_service_monitor: Refreshing state...
helm_release.woodpecker: Refreshing state... [id=woodpecker]
kubernetes_config_map_v1.grafana_loki_datasource: Refreshing state... [id=monitoring/grafana-loki-datasource]
kubernetes_ingress_v1.keycloak_funnel: Refreshing state... [id=keycloak/keycloak-funnel]
kubernetes_ingress_v1.grafana_funnel: Refreshing state... [id=monitoring/grafana-funnel]
kubernetes_ingress_v1.forgejo_funnel: Refreshing state... [id=forgejo/forgejo-funnel]
kubernetes_ingress_v1.alertmanager_funnel: Refreshing state... [id=monitoring/alertmanager-funnel]
minio_iam_policy.cnpg_wal: Refreshing state... [id=cnpg-wal]
minio_iam_user.cnpg: Refreshing state... [id=cnpg]
minio_s3_bucket.postgres_wal: Refreshing state... [id=postgres-wal]
minio_iam_user.tf_backup: Refreshing state... [id=tf-backup]
minio_s3_bucket.tf_state_backups: Refreshing state... [id=tf-state-backups]
minio_s3_bucket.assets: Refreshing state... [id=assets]
kubernetes_ingress_v1.minio_api_funnel: Refreshing state... [id=minio/minio-api-funnel]
kubernetes_ingress_v1.minio_funnel: Refreshing state... [id=minio/minio-funnel]
minio_iam_policy.tf_backup: Refreshing state... [id=tf-backup]
minio_iam_user_policy_attachment.tf_backup: Refreshing state... [id=tf-backup-20260314163610110100000001]
minio_iam_user_policy_attachment.cnpg: Refreshing state... [id=cnpg-20260302210642491000000001]
kubernetes_secret_v1.tf_backup_s3_creds: Refreshing state... [id=tofu-state/tf-backup-s3-creds]
kubernetes_secret_v1.cnpg_s3_creds: Refreshing state... [id=postgres/cnpg-s3-creds]
kubernetes_cron_job_v1.tf_state_backup: Refreshing state... [id=tofu-state/tf-state-backup]
kubernetes_ingress_v1.harbor_funnel: Refreshing state... [id=harbor/harbor-funnel]
kubernetes_ingress_v1.woodpecker_funnel: Refreshing state... [id=woodpecker/woodpecker-funnel]

OpenTofu used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  ~ update in-place

OpenTofu will perform the following actions:

  # kubernetes_cron_job_v1.tf_state_backup will be updated in-place
  ~ resource "kubernetes_cron_job_v1" "tf_state_backup" {
        id = "tofu-state/tf-state-backup"

      ~ spec {
            # (6 unchanged attributes hidden)

          ~ job_template {
              ~ spec {
                    # (7 unchanged attributes hidden)

                  ~ template {
                      ~ spec {
                            # (13 unchanged attributes hidden)

                          ~ container {
                              ~ args                       = [
                                  - <<-EOT
                                        set -euo pipefail
                                        
                                        # Install mc (MinIO Client) to /tmp (container may be non-root)
                                        curl -sSL https://dl.min.io/client/mc/release/linux-amd64/mc -o /tmp/mc
                                        chmod +x /tmp/mc
                                        
                                        # Configure MinIO alias
                                        /tmp/mc alias set backup http://minio.minio.svc.cluster.local:9000 "$AWS_ACCESS_KEY_ID" "$AWS_SECRET_ACCESS_KEY"
                                        
                                        DATE=$(date -u +%Y-%m-%d)
                                        
                                        # Backup each state secret
                                        for SECRET in tfstate-default-pal-e-platform tfstate-default-pal-e-services; do
                                          echo "Backing up $SECRET..."
                                          kubectl get secret "$SECRET" -n tofu-state -o jsonpath='{.data.tfstate}' | base64 -d > "/tmp/$SECRET-$DATE.json"
                                          /tmp/mc cp "/tmp/$SECRET-$DATE.json" "backup/tf-state-backups/$SECRET-$DATE.json"
                                          echo "Uploaded $SECRET-$DATE.json"
                                          rm -f "/tmp/$SECRET-$DATE.json"
                                        done
                                        
                                        # Prune backups older than 30 days
                                        echo "Pruning backups older than 30 days..."
                                        /tmp/mc find backup/tf-state-backups/ --older-than 30d --exec "/tmp/mc rm {}" || echo "No old backups to prune"
                                        
                                        echo "Backup complete."
                                    EOT,
                                  + <<-EOT
                                        set -euo pipefail
                                        
                                        # Alpine does not ship with curl — install it
                                        apk add --no-cache curl >/dev/null
                                        
                                        # Install mc (MinIO Client) to /tmp (container may be non-root)
                                        curl -sSL https://dl.min.io/client/mc/release/linux-amd64/mc -o /tmp/mc
                                        chmod +x /tmp/mc
                                        
                                        # Install kubectl binary
                                        curl -sSL "https://dl.k8s.io/release/v1.31.4/bin/linux/amd64/kubectl" -o /tmp/kubectl
                                        chmod +x /tmp/kubectl
                                        
                                        # Configure MinIO alias
                                        /tmp/mc alias set backup http://minio.minio.svc.cluster.local:9000 "$AWS_ACCESS_KEY_ID" "$AWS_SECRET_ACCESS_KEY"
                                        
                                        DATE=$(date -u +%Y-%m-%d)
                                        
                                        # Backup each state secret
                                        for SECRET in tfstate-default-pal-e-platform tfstate-default-pal-e-services; do
                                          echo "Backing up $SECRET..."
                                          /tmp/kubectl get secret "$SECRET" -n tofu-state -o jsonpath='{.data.tfstate}' | base64 -d > "/tmp/$SECRET-$DATE.json"
                                          /tmp/mc cp "/tmp/$SECRET-$DATE.json" "backup/tf-state-backups/$SECRET-$DATE.json"
                                          echo "Uploaded $SECRET-$DATE.json"
                                          rm -f "/tmp/$SECRET-$DATE.json"
                                        done
                                        
                                        # Prune backups older than 30 days
                                        echo "Pruning backups older than 30 days..."
                                        /tmp/mc find backup/tf-state-backups/ --older-than 30d --exec "/tmp/mc rm {}" || echo "No old backups to prune"
                                        
                                        echo "Backup complete."
                                    EOT,
                                ]
                              ~ command                    = [
                                  - "/bin/bash",
                                  + "/bin/sh",
                                    "-c",
                                ]
                              ~ image                      = "bitnami/kubectl:1.31" -> "alpine:3.20"
                                name                       = "backup"
                                # (6 unchanged attributes hidden)

                                # (2 unchanged blocks hidden)
                            }
                        }

                        # (1 unchanged block hidden)
                    }
                }

                # (1 unchanged block hidden)
            }
        }

        # (1 unchanged block hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

─────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so OpenTofu can't
guarantee to take exactly these actions if you run "tofu apply" now.
## Tofu Plan Output ``` tailscale_acl.this: Refreshing state... [id=acl] helm_release.nvidia_device_plugin: Refreshing state... [id=nvidia-device-plugin] kubernetes_namespace_v1.monitoring: Refreshing state... [id=monitoring] kubernetes_namespace_v1.woodpecker: Refreshing state... [id=woodpecker] kubernetes_namespace_v1.keycloak: Refreshing state... [id=keycloak] kubernetes_namespace_v1.ollama: Refreshing state... [id=ollama] kubernetes_namespace_v1.harbor: Refreshing state... [id=harbor] kubernetes_namespace_v1.forgejo: Refreshing state... [id=forgejo] kubernetes_namespace_v1.minio: Refreshing state... [id=minio] kubernetes_namespace_v1.postgres: Refreshing state... [id=postgres] kubernetes_namespace_v1.tailscale: Refreshing state... [id=tailscale] data.kubernetes_namespace_v1.pal_e_docs: Reading... data.kubernetes_namespace_v1.tofu_state: Reading... kubernetes_persistent_volume_claim_v1.keycloak_data: Refreshing state... [id=keycloak/keycloak-data] kubernetes_service_v1.keycloak: Refreshing state... [id=keycloak/keycloak] kubernetes_secret_v1.keycloak_admin: Refreshing state... [id=keycloak/keycloak-admin] kubernetes_namespace_v1.cnpg_system: Refreshing state... [id=cnpg-system] helm_release.forgejo: Refreshing state... [id=forgejo] data.kubernetes_namespace_v1.pal_e_docs: Read complete after 0s [id=pal-e-docs] data.kubernetes_namespace_v1.tofu_state: Read complete after 0s [id=tofu-state] kubernetes_service_v1.dora_exporter: Refreshing state... [id=monitoring/dora-exporter] helm_release.kube_prometheus_stack: Refreshing state... [id=kube-prometheus-stack] helm_release.loki_stack: Refreshing state... [id=loki-stack] kubernetes_secret_v1.dora_exporter: Refreshing state... [id=monitoring/dora-exporter] kubernetes_secret_v1.paledocs_db_url: Refreshing state... [id=pal-e-docs/paledocs-db-url] kubernetes_service_account_v1.tf_backup: Refreshing state... [id=tofu-state/tf-state-backup] kubernetes_role_v1.tf_backup: Refreshing state... [id=tofu-state/tf-state-backup] helm_release.cnpg: Refreshing state... [id=cnpg] helm_release.tailscale_operator: Refreshing state... [id=tailscale-operator] kubernetes_role_binding_v1.tf_backup: Refreshing state... [id=tofu-state/tf-state-backup] kubernetes_deployment_v1.keycloak: Refreshing state... [id=keycloak/keycloak] helm_release.ollama: Refreshing state... [id=ollama] helm_release.minio: Refreshing state... [id=minio] kubernetes_config_map_v1.dora_dashboard: Refreshing state... [id=monitoring/dora-dashboard] helm_release.harbor: Refreshing state... [id=harbor] kubernetes_deployment_v1.dora_exporter: Refreshing state... [id=monitoring/dora-exporter] kubernetes_manifest.dora_exporter_service_monitor: Refreshing state... helm_release.woodpecker: Refreshing state... [id=woodpecker] kubernetes_config_map_v1.grafana_loki_datasource: Refreshing state... [id=monitoring/grafana-loki-datasource] kubernetes_ingress_v1.keycloak_funnel: Refreshing state... [id=keycloak/keycloak-funnel] kubernetes_ingress_v1.grafana_funnel: Refreshing state... [id=monitoring/grafana-funnel] kubernetes_ingress_v1.forgejo_funnel: Refreshing state... [id=forgejo/forgejo-funnel] kubernetes_ingress_v1.alertmanager_funnel: Refreshing state... [id=monitoring/alertmanager-funnel] minio_iam_policy.cnpg_wal: Refreshing state... [id=cnpg-wal] minio_iam_user.cnpg: Refreshing state... [id=cnpg] minio_s3_bucket.postgres_wal: Refreshing state... [id=postgres-wal] minio_iam_user.tf_backup: Refreshing state... [id=tf-backup] minio_s3_bucket.tf_state_backups: Refreshing state... [id=tf-state-backups] minio_s3_bucket.assets: Refreshing state... [id=assets] kubernetes_ingress_v1.minio_api_funnel: Refreshing state... [id=minio/minio-api-funnel] kubernetes_ingress_v1.minio_funnel: Refreshing state... [id=minio/minio-funnel] minio_iam_policy.tf_backup: Refreshing state... [id=tf-backup] minio_iam_user_policy_attachment.tf_backup: Refreshing state... [id=tf-backup-20260314163610110100000001] minio_iam_user_policy_attachment.cnpg: Refreshing state... [id=cnpg-20260302210642491000000001] kubernetes_secret_v1.tf_backup_s3_creds: Refreshing state... [id=tofu-state/tf-backup-s3-creds] kubernetes_secret_v1.cnpg_s3_creds: Refreshing state... [id=postgres/cnpg-s3-creds] kubernetes_cron_job_v1.tf_state_backup: Refreshing state... [id=tofu-state/tf-state-backup] kubernetes_ingress_v1.harbor_funnel: Refreshing state... [id=harbor/harbor-funnel] kubernetes_ingress_v1.woodpecker_funnel: Refreshing state... [id=woodpecker/woodpecker-funnel] OpenTofu used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: ~ update in-place OpenTofu will perform the following actions: # kubernetes_cron_job_v1.tf_state_backup will be updated in-place ~ resource "kubernetes_cron_job_v1" "tf_state_backup" { id = "tofu-state/tf-state-backup" ~ spec { # (6 unchanged attributes hidden) ~ job_template { ~ spec { # (7 unchanged attributes hidden) ~ template { ~ spec { # (13 unchanged attributes hidden) ~ container { ~ args = [ - <<-EOT set -euo pipefail # Install mc (MinIO Client) to /tmp (container may be non-root) curl -sSL https://dl.min.io/client/mc/release/linux-amd64/mc -o /tmp/mc chmod +x /tmp/mc # Configure MinIO alias /tmp/mc alias set backup http://minio.minio.svc.cluster.local:9000 "$AWS_ACCESS_KEY_ID" "$AWS_SECRET_ACCESS_KEY" DATE=$(date -u +%Y-%m-%d) # Backup each state secret for SECRET in tfstate-default-pal-e-platform tfstate-default-pal-e-services; do echo "Backing up $SECRET..." kubectl get secret "$SECRET" -n tofu-state -o jsonpath='{.data.tfstate}' | base64 -d > "/tmp/$SECRET-$DATE.json" /tmp/mc cp "/tmp/$SECRET-$DATE.json" "backup/tf-state-backups/$SECRET-$DATE.json" echo "Uploaded $SECRET-$DATE.json" rm -f "/tmp/$SECRET-$DATE.json" done # Prune backups older than 30 days echo "Pruning backups older than 30 days..." /tmp/mc find backup/tf-state-backups/ --older-than 30d --exec "/tmp/mc rm {}" || echo "No old backups to prune" echo "Backup complete." EOT, + <<-EOT set -euo pipefail # Alpine does not ship with curl — install it apk add --no-cache curl >/dev/null # Install mc (MinIO Client) to /tmp (container may be non-root) curl -sSL https://dl.min.io/client/mc/release/linux-amd64/mc -o /tmp/mc chmod +x /tmp/mc # Install kubectl binary curl -sSL "https://dl.k8s.io/release/v1.31.4/bin/linux/amd64/kubectl" -o /tmp/kubectl chmod +x /tmp/kubectl # Configure MinIO alias /tmp/mc alias set backup http://minio.minio.svc.cluster.local:9000 "$AWS_ACCESS_KEY_ID" "$AWS_SECRET_ACCESS_KEY" DATE=$(date -u +%Y-%m-%d) # Backup each state secret for SECRET in tfstate-default-pal-e-platform tfstate-default-pal-e-services; do echo "Backing up $SECRET..." /tmp/kubectl get secret "$SECRET" -n tofu-state -o jsonpath='{.data.tfstate}' | base64 -d > "/tmp/$SECRET-$DATE.json" /tmp/mc cp "/tmp/$SECRET-$DATE.json" "backup/tf-state-backups/$SECRET-$DATE.json" echo "Uploaded $SECRET-$DATE.json" rm -f "/tmp/$SECRET-$DATE.json" done # Prune backups older than 30 days echo "Pruning backups older than 30 days..." /tmp/mc find backup/tf-state-backups/ --older-than 30d --exec "/tmp/mc rm {}" || echo "No old backups to prune" echo "Backup complete." EOT, ] ~ command = [ - "/bin/bash", + "/bin/sh", "-c", ] ~ image = "bitnami/kubectl:1.31" -> "alpine:3.20" name = "backup" # (6 unchanged attributes hidden) # (2 unchanged blocks hidden) } } # (1 unchanged block hidden) } } # (1 unchanged block hidden) } } # (1 unchanged block hidden) } Plan: 0 to add, 1 to change, 0 to destroy. ───────────────────────────────────────────────────────────────────────────── Note: You didn't use the -out option to save this plan, so OpenTofu can't guarantee to take exactly these actions if you run "tofu apply" now. ```
Author
Owner

PR #52 Review

Title: Fix tf-state-backup CronJob -- replace dead bitnami/kubectl image
Branch: 51-fix-tf-state-backup-cronjob-replace-dead
Changed files: 1 (terraform/main.tf), +10/-3

BLOCKERS

None.

NITS

  1. No checksum verification on downloaded binaries. Both kubectl and mc are downloaded via curl without SHA256 verification. An MITM or CDN compromise could inject a malicious binary. This matches the existing pattern for mc (pre-existing in PR #39), so it is not a regression, but worth noting for a future hardening pass. Consider adding sha256sum verification for both binaries in a follow-up.

  2. kubectl version pinning. v1.31.4 is hardcoded in the URL. When the cluster upgrades, this will need a manual bump. A minor maintenance note, not a blocker -- the old bitnami/kubectl:1.31 had the same coupling.

SOP COMPLIANCE

  • Branch named after issue (51-fix-tf-state-backup-cronjob-replace-dead references #51)
  • PR body follows template (Summary, Changes, Test Plan, Related sections all present)
  • Related references plan slug (plan-pal-e-platform)
  • Closes #51 present in PR body
  • No secrets committed (only terraform/main.tf modified)
  • No scope creep (1 file, minimal targeted change)
  • tofu fmt and tofu validate reported as passed

TECHNICAL REVIEW

Check Result
Image bitnami/kubectl:1.31 -> alpine:3.20 Correct
Shell /bin/bash -> /bin/sh Correct (Alpine uses BusyBox ash)
apk add --no-cache curl added Correct (Alpine minimal has no curl)
kubectl downloaded to /tmp/kubectl via official URL Correct (https://dl.k8s.io/release/v1.31.4/bin/linux/amd64/kubectl)
mc download pattern preserved Correct (unchanged)
Bare kubectl replaced with /tmp/kubectl Correct (1 occurrence in the backup loop)
set -euo pipefail with BusyBox ash Valid (BusyBox ash supports pipefail since 1.16; Alpine 3.20 ships a much newer version)
Download URLs use HTTPS Correct (both dl.k8s.io and dl.min.io)
Alpine image pinned to specific version Correct (3.20, no :latest)

The change is minimal, targeted, and follows the existing mc download pattern already established in the CronJob. The root cause (Bitnami removing all Docker Hub images) is well-documented in MEMORY.md and the PR description.

VERDICT: APPROVED

## PR #52 Review **Title:** Fix tf-state-backup CronJob -- replace dead bitnami/kubectl image **Branch:** `51-fix-tf-state-backup-cronjob-replace-dead` **Changed files:** 1 (`terraform/main.tf`), +10/-3 ### BLOCKERS None. ### NITS 1. **No checksum verification on downloaded binaries.** Both `kubectl` and `mc` are downloaded via curl without SHA256 verification. An MITM or CDN compromise could inject a malicious binary. This matches the existing pattern for `mc` (pre-existing in PR #39), so it is not a regression, but worth noting for a future hardening pass. Consider adding `sha256sum` verification for both binaries in a follow-up. 2. **kubectl version pinning.** `v1.31.4` is hardcoded in the URL. When the cluster upgrades, this will need a manual bump. A minor maintenance note, not a blocker -- the old `bitnami/kubectl:1.31` had the same coupling. ### SOP COMPLIANCE - [x] Branch named after issue (`51-fix-tf-state-backup-cronjob-replace-dead` references #51) - [x] PR body follows template (Summary, Changes, Test Plan, Related sections all present) - [x] Related references plan slug (`plan-pal-e-platform`) - [x] `Closes #51` present in PR body - [x] No secrets committed (only `terraform/main.tf` modified) - [x] No scope creep (1 file, minimal targeted change) - [x] `tofu fmt` and `tofu validate` reported as passed ### TECHNICAL REVIEW | Check | Result | |-------|--------| | Image `bitnami/kubectl:1.31` -> `alpine:3.20` | Correct | | Shell `/bin/bash` -> `/bin/sh` | Correct (Alpine uses BusyBox ash) | | `apk add --no-cache curl` added | Correct (Alpine minimal has no curl) | | kubectl downloaded to `/tmp/kubectl` via official URL | Correct (`https://dl.k8s.io/release/v1.31.4/bin/linux/amd64/kubectl`) | | mc download pattern preserved | Correct (unchanged) | | Bare `kubectl` replaced with `/tmp/kubectl` | Correct (1 occurrence in the backup loop) | | `set -euo pipefail` with BusyBox ash | Valid (BusyBox ash supports pipefail since 1.16; Alpine 3.20 ships a much newer version) | | Download URLs use HTTPS | Correct (both dl.k8s.io and dl.min.io) | | Alpine image pinned to specific version | Correct (3.20, no :latest) | The change is minimal, targeted, and follows the existing mc download pattern already established in the CronJob. The root cause (Bitnami removing all Docker Hub images) is well-documented in MEMORY.md and the PR description. ### VERDICT: APPROVED
forgejo_admin deleted branch 51-fix-tf-state-backup-cronjob-replace-dead 2026-03-14 18:25:52 +00:00
Sign in to join this conversation.
No description provided.