Add Arch Linux Ruby base image with weekly rebuild pipeline #361

Merged
ldraney merged 1 commit from 360-arch-ruby-base-image into main 2026-05-10 16:47:22 +00:00
Owner

Summary

  • Shared Arch Linux Ruby base image for all Rails apps, pushed to Harbor
  • Multi-stage Dockerfile: runtime (ruby, jemalloc, postgresql-libs) and build (base-devel, git, pkgconf)
  • Woodpecker pipeline for manual + weekly cron rebuild via Kaniko

Changes

  • docker/ruby-arch/Dockerfile: Multi-stage base image. base stage installs ruby, jemalloc, postgresql-libs, libyaml, and bundler via gem. build stage adds base-devel, git, pkgconf for native gem compilation.
  • docker/ruby-arch/.woodpecker.yaml: Kaniko-based pipeline matching sibling repo conventions (basketball-api, pal-e-docs). Triggers on manual + cron events, pushes to pal-e/ruby-arch:latest.

Test Plan

  • Dockerfile builds successfully via Kaniko
  • ruby --version in container returns current Arch Ruby
  • bundler --version works
  • gem install pg works in build stage
  • LD_PRELOAD=/usr/lib/libjemalloc.so ruby -e "puts :ok" succeeds

Review Checklist

  • Passed automated review-fix loop
  • No secrets committed
  • No unnecessary file changes
  • Commit messages are descriptive
  • ldraney/pal-e-platform #360 -- Arch Linux Ruby base image in Harbor with weekly rebuild pipeline

Closes #360

## Summary - Shared Arch Linux Ruby base image for all Rails apps, pushed to Harbor - Multi-stage Dockerfile: runtime (ruby, jemalloc, postgresql-libs) and build (base-devel, git, pkgconf) - Woodpecker pipeline for manual + weekly cron rebuild via Kaniko ## Changes - `docker/ruby-arch/Dockerfile`: Multi-stage base image. `base` stage installs ruby, jemalloc, postgresql-libs, libyaml, and bundler via gem. `build` stage adds base-devel, git, pkgconf for native gem compilation. - `docker/ruby-arch/.woodpecker.yaml`: Kaniko-based pipeline matching sibling repo conventions (basketball-api, pal-e-docs). Triggers on manual + cron events, pushes to `pal-e/ruby-arch:latest`. ## Test Plan - [ ] Dockerfile builds successfully via Kaniko - [ ] `ruby --version` in container returns current Arch Ruby - [ ] `bundler --version` works - [ ] `gem install pg` works in build stage - [ ] `LD_PRELOAD=/usr/lib/libjemalloc.so ruby -e "puts :ok"` succeeds ## Review Checklist - [ ] Passed automated review-fix loop - [ ] No secrets committed - [ ] No unnecessary file changes - [ ] Commit messages are descriptive ## Related Notes - `ldraney/pal-e-platform #360` -- Arch Linux Ruby base image in Harbor with weekly rebuild pipeline Closes #360
Add Arch Linux Ruby base image with weekly rebuild pipeline
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
d0309596f5
Multi-stage Dockerfile for shared Ruby runtime:
- base: archlinux + ruby, jemalloc, postgresql-libs, libyaml, bundler
- build: adds base-devel, git, pkgconf for native gem compilation

Woodpecker pipeline uses Kaniko to build and push to
harbor.harbor.svc.cluster.local/pal-e/ruby-arch:latest on manual
trigger and weekly cron.

Closes #360

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Contributor

Tofu Plan Output (full)

module.networking.tailscale_acl.this: Refreshing state... [id=acl]
module.forgejo.kubernetes_namespace_v1.forgejo: Refreshing state... [id=forgejo]
module.monitoring.kubernetes_namespace_v1.monitoring: Refreshing state... [id=monitoring]
module.networking.kubernetes_namespace_v1.tailscale: Refreshing state... [id=tailscale]
module.database.data.kubernetes_namespace_v1.basketball_api: Reading...
module.database.kubernetes_namespace_v1.postgres: Refreshing state... [id=postgres]
module.database.data.kubernetes_namespace_v1.westside_admin: Reading...
module.database.kubernetes_namespace_v1.cnpg_system: Refreshing state... [id=cnpg-system]
module.staging.kubernetes_namespace_v1.staging: Refreshing state... [id=staging]
module.database.data.kubernetes_namespace_v1.basketball_api: Read complete after 0s [id=basketball-api]
module.database.data.kubernetes_namespace_v1.pal_e_production: Reading...
module.database.data.kubernetes_namespace_v1.westside_admin: Read complete after 0s [id=westside-admin]
module.database.data.kubernetes_namespace_v1.pal_e_production: Read complete after 0s [id=pal-e-app]
module.keycloak.kubernetes_namespace_v1.keycloak: Refreshing state... [id=keycloak]
module.monitoring.kubernetes_secret_v1.dora_exporter: Refreshing state... [id=monitoring/dora-exporter]
module.monitoring.kubernetes_secret_v1.grafana_oidc: Refreshing state... [id=monitoring/grafana-oidc]
module.monitoring.kubernetes_service_v1.dora_exporter: Refreshing state... [id=monitoring/dora-exporter]
module.monitoring.kubernetes_config_map_v1.uptime_dashboard: Refreshing state... [id=monitoring/uptime-dashboard]
module.monitoring.helm_release.kube_prometheus_stack: Refreshing state... [id=kube-prometheus-stack]
module.monitoring.helm_release.loki_stack: Refreshing state... [id=loki-stack]
module.database.kubernetes_job_v1.admin_app_user_provision: Refreshing state... [id=basketball-api/admin-app-user-provision-c5662180]
module.database.kubernetes_secret_v1.paledocs_db_url: Refreshing state... [id=pal-e-app/paledocs-db-url]
module.forgejo.kubernetes_config_map_v1.forgejo_custom_css: Refreshing state... [id=forgejo/forgejo-custom-css]
module.database.helm_release.cnpg: Refreshing state... [id=cnpg]
module.networking.helm_release.tailscale_operator: Refreshing state... [id=tailscale-operator]
module.forgejo.kubernetes_secret_v1.forgejo_oidc: Refreshing state... [id=forgejo/forgejo-oidc]
module.keycloak.kubernetes_persistent_volume_claim_v1.keycloak_data: Refreshing state... [id=keycloak/keycloak-data]
module.keycloak.kubernetes_config_map_v1.keycloak_westside_theme: Refreshing state... [id=keycloak/keycloak-westside-theme]
module.keycloak.kubernetes_service_v1.keycloak: Refreshing state... [id=keycloak/keycloak]
module.keycloak.kubernetes_secret_v1.keycloak_admin: Refreshing state... [id=keycloak/keycloak-admin]
kubernetes_manifest.netpol_basketball_api: Refreshing state...
kubernetes_manifest.netpol_monitoring: Refreshing state...
kubernetes_manifest.netpol_postgres: Refreshing state...
kubernetes_manifest.netpol_staging: Refreshing state...
kubernetes_manifest.netpol_cnpg_system: Refreshing state...
kubernetes_manifest.netpol_forgejo: Refreshing state...
module.forgejo.helm_release.forgejo: Refreshing state... [id=forgejo]
module.database.kubernetes_secret_v1.admin_app_db_url: Refreshing state... [id=basketball-api/admin-app-db-url]
module.database.kubernetes_secret_v1.admin_app_db_url_westside_admin: Refreshing state... [id=westside-admin/admin-app-db-url]
kubernetes_manifest.netpol_keycloak: Refreshing state...
module.keycloak.kubernetes_deployment_v1.keycloak: Refreshing state... [id=keycloak/keycloak]
module.admin.kubernetes_namespace_v1.admin: Refreshing state... [id=pal-e-admin]
module.admin.kubernetes_secret_v1.admin_auth: Refreshing state... [id=pal-e-admin/admin-auth]
module.admin.kubernetes_secret_v1.harbor_creds: Refreshing state... [id=pal-e-admin/harbor-creds]
module.admin.kubernetes_service_v1.admin: Refreshing state... [id=pal-e-admin/pal-e-admin]
module.admin.kubernetes_deployment_v1.admin: Refreshing state... [id=pal-e-admin/pal-e-admin]
module.monitoring.kubernetes_config_map_v1.basketball_api_dashboard: Refreshing state... [id=monitoring/basketball-api-dashboard]
module.monitoring.kubernetes_config_map_v1.pal_e_production_dashboard: Refreshing state... [id=monitoring/pal-e-app-dashboard]
module.monitoring.kubernetes_config_map_v1.mac_agent_dashboard: Refreshing state... [id=monitoring/mac-agent-dashboard]
module.monitoring.kubernetes_config_map_v1.playme2k_dashboard: Refreshing state... [id=monitoring/playme2k-dashboard]
module.monitoring.kubernetes_manifest.embedding_alerts: Refreshing state...
module.monitoring.kubernetes_manifest.blackbox_alerts: Refreshing state...
module.monitoring.kubernetes_manifest.gmail_oauth_expiry_alert: Refreshing state...
module.monitoring.helm_release.blackbox_exporter: Refreshing state... [id=blackbox-exporter]
module.monitoring.kubernetes_config_map_v1.grafana_loki_datasource: Refreshing state... [id=monitoring/grafana-loki-datasource]
module.monitoring.kubernetes_manifest.payment_pipeline_alerts: Refreshing state...
module.monitoring.kubernetes_config_map_v1.dora_dashboard: Refreshing state... [id=monitoring/dora-dashboard]
module.monitoring.kubernetes_deployment_v1.dora_exporter: Refreshing state... [id=monitoring/dora-exporter]
module.monitoring.kubernetes_manifest.dora_exporter_service_monitor: Refreshing state...
module.monitoring.kubernetes_manifest.embedding_worker_service_monitor: Refreshing state...
module.networking.kubernetes_ingress_v1.keycloak_funnel: Refreshing state... [id=keycloak/keycloak-funnel]
module.networking.kubernetes_ingress_v1.admin_funnel: Refreshing state... [id=pal-e-admin/admin-funnel]
module.networking.kubernetes_ingress_v1.grafana_funnel: Refreshing state... [id=monitoring/grafana-funnel]
module.networking.kubernetes_ingress_v1.alertmanager_funnel: Refreshing state... [id=monitoring/alertmanager-funnel]
module.networking.kubernetes_ingress_v1.forgejo_funnel: Refreshing state... [id=forgejo/forgejo-funnel]
module.networking.kubernetes_manifest.tailscale_subnet_router: Refreshing state...
module.storage.kubernetes_namespace_v1.minio: Refreshing state... [id=minio]
module.harbor.kubernetes_namespace_v1.harbor: Refreshing state... [id=harbor]
module.harbor.kubernetes_config_map_v1.harbor_portal_nginx: Refreshing state... [id=harbor/harbor-portal-nginx]
module.harbor.kubernetes_config_map_v1.harbor_portal_css: Refreshing state... [id=harbor/harbor-portal-css]
module.networking.kubernetes_ingress_v1.harbor_funnel: Refreshing state... [id=harbor/harbor-funnel]
module.harbor.kubernetes_service_v1.harbor_portal_proxy: Refreshing state... [id=harbor/harbor-portal-proxy]
module.harbor.helm_release.harbor: Refreshing state... [id=harbor]
kubernetes_manifest.netpol_harbor: Refreshing state...
module.storage.kubernetes_config_map_v1.minio_console_nginx: Refreshing state... [id=minio/minio-console-nginx]
module.storage.helm_release.minio: Refreshing state... [id=minio]
module.storage.kubernetes_config_map_v1.minio_console_css: Refreshing state... [id=minio/minio-console-css]
module.storage.kubernetes_service_v1.minio_console_proxy: Refreshing state... [id=minio/minio-console-proxy]
module.networking.kubernetes_ingress_v1.minio_api_funnel: Refreshing state... [id=minio/minio-api-funnel]
module.networking.kubernetes_ingress_v1.minio_funnel: Refreshing state... [id=minio/minio-funnel]
kubernetes_manifest.netpol_minio: Refreshing state...
module.harbor.null_resource.harbor_oidc: Refreshing state... [id=2304382569774565098]
module.harbor.kubernetes_deployment_v1.harbor_portal_proxy: Refreshing state... [id=harbor/harbor-portal-proxy]
module.storage.minio_s3_bucket.assets: Refreshing state... [id=assets]
module.storage.minio_iam_policy.cnpg_wal: Refreshing state... [id=cnpg-wal]
module.storage.minio_iam_user.cnpg: Refreshing state... [id=cnpg]
module.storage.minio_s3_bucket.tf_state_backups: Refreshing state... [id=tf-state-backups]
module.storage.minio_iam_policy.tf_backup: Refreshing state... [id=tf-backup]
module.storage.minio_iam_user.tf_backup: Refreshing state... [id=tf-backup]
module.storage.minio_s3_bucket.postgres_wal: Refreshing state... [id=postgres-wal]
module.database.kubernetes_secret_v1.cnpg_s3_creds: Refreshing state... [id=postgres/cnpg-s3-creds]
module.storage.minio_iam_user_policy_attachment.cnpg: Refreshing state... [id=cnpg-20260302210642491000000001]
module.storage.minio_s3_bucket_policy.assets_public_read: Refreshing state... [id=assets]
module.storage.kubernetes_deployment_v1.minio_console_proxy: Refreshing state... [id=minio/minio-console-proxy]
module.storage.minio_iam_user_policy_attachment.tf_backup: Refreshing state... [id=tf-backup-20260314163610110100000001]
module.database.kubernetes_cron_job_v1.cnpg_backup_verify: Refreshing state... [id=postgres/cnpg-backup-verify]
module.ops.kubernetes_namespace_v1.ollama: Refreshing state... [id=ollama]
module.ops.data.kubernetes_namespace_v1.tofu_state: Reading...
module.ops.kubernetes_service_v1.embedding_worker_metrics: Refreshing state... [id=pal-e-app/embedding-worker-metrics]
module.ops.helm_release.nvidia_device_plugin: Refreshing state... [id=nvidia-device-plugin]
module.ops.data.kubernetes_namespace_v1.tofu_state: Read complete after 0s [id=tofu-state]
module.ops.kubernetes_role_v1.tf_backup: Refreshing state... [id=tofu-state/tf-state-backup]
module.ops.kubernetes_service_account_v1.tf_backup: Refreshing state... [id=tofu-state/tf-state-backup]
module.ops.kubernetes_secret_v1.tf_backup_s3_creds: Refreshing state... [id=tofu-state/tf-backup-s3-creds]
module.ci.kubernetes_namespace_v1.woodpecker: Refreshing state... [id=woodpecker]
module.ops.kubernetes_role_binding_v1.tf_backup: Refreshing state... [id=tofu-state/tf-state-backup]
kubernetes_manifest.netpol_ollama: Refreshing state...
module.ci.kubernetes_secret_v1.woodpecker_db_credentials: Refreshing state... [id=woodpecker/woodpecker-db-credentials]
module.ci.kubernetes_secret_v1.woodpecker_cnpg_s3_creds: Refreshing state... [id=woodpecker/cnpg-s3-creds]
module.ops.kubernetes_cron_job_v1.tf_state_backup: Refreshing state... [id=tofu-state/tf-state-backup]
module.networking.kubernetes_ingress_v1.woodpecker_funnel: Refreshing state... [id=woodpecker/woodpecker-funnel]
kubernetes_manifest.netpol_woodpecker: Refreshing state...
module.ci.kubernetes_manifest.woodpecker_postgres: Refreshing state...
module.ops.helm_release.ollama: Refreshing state... [id=ollama]
module.ci.helm_release.woodpecker: Refreshing state... [id=woodpecker]
module.ci.kubernetes_manifest.woodpecker_postgres_scheduled_backup: Refreshing state...
module.ci.kubernetes_manifest.woodpecker_postgres_podmonitor: Refreshing state...

OpenTofu used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create
-/+ destroy and then create replacement

OpenTofu will perform the following actions:

  # module.database.kubernetes_job_v1.admin_app_user_provision will be created
  + resource "kubernetes_job_v1" "admin_app_user_provision" {
      + id                  = (known after apply)
      + wait_for_completion = true

      + metadata {
          + generation       = (known after apply)
          + labels           = {
              + "app.kubernetes.io/managed-by" = "pal-e-platform-terraform"
              + "app.kubernetes.io/name"       = "admin-app-user-provision"
              + "arch"                         = "postgres"
              + "story"                        = "admin-row-crud"
            }
          + name             = (sensitive value)
          + namespace        = "basketball-api"
          + resource_version = (known after apply)
          + uid              = (known after apply)
        }

      + spec {
          + backoff_limit              = 4
          + completion_mode            = (known after apply)
          + completions                = 1
          + parallelism                = 1
          + ttl_seconds_after_finished = "3600"

          + selector (known after apply)

          + template {
              + metadata {
                  + generation       = (known after apply)
                  + labels           = {
                      + "app.kubernetes.io/name" = "admin-app-user-provision"
                    }
                  + name             = (known after apply)
                  + resource_version = (known after apply)
                  + uid              = (known after apply)
                }
              + spec {
                  + automount_service_account_token  = true
                  + dns_policy                       = "ClusterFirst"
                  + enable_service_links             = true
                  + host_ipc                         = false
                  + host_network                     = false
                  + host_pid                         = false
                  + hostname                         = (known after apply)
                  + node_name                        = (known after apply)
                  + restart_policy                   = "OnFailure"
                  + scheduler_name                   = (known after apply)
                  + service_account_name             = (known after apply)
                  + share_process_namespace          = false
                  + termination_grace_period_seconds = 30

                  + container {
                      + args                       = [
                          + <<-EOT
                                set -euo pipefail
                                
                                echo "==> Provisioning admin_app role on ${PGHOST}/${PGDATABASE}"
                                
                                # Idempotent: CREATE ROLE if missing, otherwise rotate password.
                                psql -v ON_ERROR_STOP=1 -v admin_pw="${ADMIN_APP_PASSWORD}" <<'SQL'
                                  -- Idempotent role creation/rotation. Conditional uses psql's \gset + \if
                                  -- (client-side) so we can use :'admin_pw' substitution. psql variable
                                  -- substitution does NOT work inside DO $$...$$ dollar-quoted blocks.
                                  SELECT EXISTS(SELECT 1 FROM pg_roles WHERE rolname = 'admin_app') AS role_exists \gset
                                  \if :role_exists
                                    ALTER ROLE admin_app WITH LOGIN PASSWORD :'admin_pw';
                                  \else
                                    CREATE ROLE admin_app WITH LOGIN PASSWORD :'admin_pw';
                                  \endif
                                
                                  -- DML-only grants on schema public. Idempotent (re-grant is a no-op).
                                  GRANT USAGE ON SCHEMA public TO admin_app;
                                  GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO admin_app;
                                  GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO admin_app;
                                
                                  -- Forward grants for tables/sequences created later by Drizzle migrations
                                  -- (which run as the basketball superuser).
                                  ALTER DEFAULT PRIVILEGES FOR ROLE basketball IN SCHEMA public
                                    GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO admin_app;
                                  ALTER DEFAULT PRIVILEGES FOR ROLE basketball IN SCHEMA public
                                    GRANT USAGE ON SEQUENCES TO admin_app;
                                SQL
                                
                                echo "==> admin_app role provisioned successfully"
                            EOT,
                        ]
                      + command                    = [
                          + "/bin/sh",
                          + "-c",
                        ]
                      + image                      = "postgres:16-alpine"
                      + image_pull_policy          = (known after apply)
                      + name                       = "psql"
                      + stdin                      = false
                      + stdin_once                 = false
                      + termination_message_path   = "/dev/termination-log"
                      + termination_message_policy = (known after apply)
                      + tty                        = false

                      + env {
                          + name  = "PGHOST"
                          + value = "postgres.basketball-api.svc.cluster.local"
                        }
                      + env {
                          + name  = "PGPORT"
                          + value = "5432"
                        }
                      + env {
                          + name  = "PGUSER"
                          + value = "basketball"
                        }
                      + env {
                          + name  = "PGDATABASE"
                          + value = "basketball"
                        }
                      + env {
                          + name = "PGPASSWORD"

                          + value_from {
                              + secret_key_ref {
                                  + key  = "postgres-password"
                                  + name = "basketball-api-secrets"
                                }
                            }
                        }
                      + env {
                          + name  = "ADMIN_APP_PASSWORD"
                          + value = (sensitive value)
                        }

                      + resources {
                          + limits   = {
                              + "memory" = "128Mi"
                            }
                          + requests = {
                              + "cpu"    = "50m"
                              + "memory" = "64Mi"
                            }
                        }
                    }

                  + image_pull_secrets (known after apply)

                  + readiness_gate (known after apply)
                }
            }
        }

      + timeouts {
          + create = "5m"
          + update = "5m"
        }
    }

  # module.harbor.null_resource.harbor_oidc is tainted, so it must be replaced
-/+ resource "null_resource" "harbor_oidc" {
      ~ id       = "2304382569774565098" -> (known after apply)
        # (1 unchanged attribute hidden)
    }

Plan: 2 to add, 0 to change, 1 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 (full) ``` module.networking.tailscale_acl.this: Refreshing state... [id=acl] module.forgejo.kubernetes_namespace_v1.forgejo: Refreshing state... [id=forgejo] module.monitoring.kubernetes_namespace_v1.monitoring: Refreshing state... [id=monitoring] module.networking.kubernetes_namespace_v1.tailscale: Refreshing state... [id=tailscale] module.database.data.kubernetes_namespace_v1.basketball_api: Reading... module.database.kubernetes_namespace_v1.postgres: Refreshing state... [id=postgres] module.database.data.kubernetes_namespace_v1.westside_admin: Reading... module.database.kubernetes_namespace_v1.cnpg_system: Refreshing state... [id=cnpg-system] module.staging.kubernetes_namespace_v1.staging: Refreshing state... [id=staging] module.database.data.kubernetes_namespace_v1.basketball_api: Read complete after 0s [id=basketball-api] module.database.data.kubernetes_namespace_v1.pal_e_production: Reading... module.database.data.kubernetes_namespace_v1.westside_admin: Read complete after 0s [id=westside-admin] module.database.data.kubernetes_namespace_v1.pal_e_production: Read complete after 0s [id=pal-e-app] module.keycloak.kubernetes_namespace_v1.keycloak: Refreshing state... [id=keycloak] module.monitoring.kubernetes_secret_v1.dora_exporter: Refreshing state... [id=monitoring/dora-exporter] module.monitoring.kubernetes_secret_v1.grafana_oidc: Refreshing state... [id=monitoring/grafana-oidc] module.monitoring.kubernetes_service_v1.dora_exporter: Refreshing state... [id=monitoring/dora-exporter] module.monitoring.kubernetes_config_map_v1.uptime_dashboard: Refreshing state... [id=monitoring/uptime-dashboard] module.monitoring.helm_release.kube_prometheus_stack: Refreshing state... [id=kube-prometheus-stack] module.monitoring.helm_release.loki_stack: Refreshing state... [id=loki-stack] module.database.kubernetes_job_v1.admin_app_user_provision: Refreshing state... [id=basketball-api/admin-app-user-provision-c5662180] module.database.kubernetes_secret_v1.paledocs_db_url: Refreshing state... [id=pal-e-app/paledocs-db-url] module.forgejo.kubernetes_config_map_v1.forgejo_custom_css: Refreshing state... [id=forgejo/forgejo-custom-css] module.database.helm_release.cnpg: Refreshing state... [id=cnpg] module.networking.helm_release.tailscale_operator: Refreshing state... [id=tailscale-operator] module.forgejo.kubernetes_secret_v1.forgejo_oidc: Refreshing state... [id=forgejo/forgejo-oidc] module.keycloak.kubernetes_persistent_volume_claim_v1.keycloak_data: Refreshing state... [id=keycloak/keycloak-data] module.keycloak.kubernetes_config_map_v1.keycloak_westside_theme: Refreshing state... [id=keycloak/keycloak-westside-theme] module.keycloak.kubernetes_service_v1.keycloak: Refreshing state... [id=keycloak/keycloak] module.keycloak.kubernetes_secret_v1.keycloak_admin: Refreshing state... [id=keycloak/keycloak-admin] kubernetes_manifest.netpol_basketball_api: Refreshing state... kubernetes_manifest.netpol_monitoring: Refreshing state... kubernetes_manifest.netpol_postgres: Refreshing state... kubernetes_manifest.netpol_staging: Refreshing state... kubernetes_manifest.netpol_cnpg_system: Refreshing state... kubernetes_manifest.netpol_forgejo: Refreshing state... module.forgejo.helm_release.forgejo: Refreshing state... [id=forgejo] module.database.kubernetes_secret_v1.admin_app_db_url: Refreshing state... [id=basketball-api/admin-app-db-url] module.database.kubernetes_secret_v1.admin_app_db_url_westside_admin: Refreshing state... [id=westside-admin/admin-app-db-url] kubernetes_manifest.netpol_keycloak: Refreshing state... module.keycloak.kubernetes_deployment_v1.keycloak: Refreshing state... [id=keycloak/keycloak] module.admin.kubernetes_namespace_v1.admin: Refreshing state... [id=pal-e-admin] module.admin.kubernetes_secret_v1.admin_auth: Refreshing state... [id=pal-e-admin/admin-auth] module.admin.kubernetes_secret_v1.harbor_creds: Refreshing state... [id=pal-e-admin/harbor-creds] module.admin.kubernetes_service_v1.admin: Refreshing state... [id=pal-e-admin/pal-e-admin] module.admin.kubernetes_deployment_v1.admin: Refreshing state... [id=pal-e-admin/pal-e-admin] module.monitoring.kubernetes_config_map_v1.basketball_api_dashboard: Refreshing state... [id=monitoring/basketball-api-dashboard] module.monitoring.kubernetes_config_map_v1.pal_e_production_dashboard: Refreshing state... [id=monitoring/pal-e-app-dashboard] module.monitoring.kubernetes_config_map_v1.mac_agent_dashboard: Refreshing state... [id=monitoring/mac-agent-dashboard] module.monitoring.kubernetes_config_map_v1.playme2k_dashboard: Refreshing state... [id=monitoring/playme2k-dashboard] module.monitoring.kubernetes_manifest.embedding_alerts: Refreshing state... module.monitoring.kubernetes_manifest.blackbox_alerts: Refreshing state... module.monitoring.kubernetes_manifest.gmail_oauth_expiry_alert: Refreshing state... module.monitoring.helm_release.blackbox_exporter: Refreshing state... [id=blackbox-exporter] module.monitoring.kubernetes_config_map_v1.grafana_loki_datasource: Refreshing state... [id=monitoring/grafana-loki-datasource] module.monitoring.kubernetes_manifest.payment_pipeline_alerts: Refreshing state... module.monitoring.kubernetes_config_map_v1.dora_dashboard: Refreshing state... [id=monitoring/dora-dashboard] module.monitoring.kubernetes_deployment_v1.dora_exporter: Refreshing state... [id=monitoring/dora-exporter] module.monitoring.kubernetes_manifest.dora_exporter_service_monitor: Refreshing state... module.monitoring.kubernetes_manifest.embedding_worker_service_monitor: Refreshing state... module.networking.kubernetes_ingress_v1.keycloak_funnel: Refreshing state... [id=keycloak/keycloak-funnel] module.networking.kubernetes_ingress_v1.admin_funnel: Refreshing state... [id=pal-e-admin/admin-funnel] module.networking.kubernetes_ingress_v1.grafana_funnel: Refreshing state... [id=monitoring/grafana-funnel] module.networking.kubernetes_ingress_v1.alertmanager_funnel: Refreshing state... [id=monitoring/alertmanager-funnel] module.networking.kubernetes_ingress_v1.forgejo_funnel: Refreshing state... [id=forgejo/forgejo-funnel] module.networking.kubernetes_manifest.tailscale_subnet_router: Refreshing state... module.storage.kubernetes_namespace_v1.minio: Refreshing state... [id=minio] module.harbor.kubernetes_namespace_v1.harbor: Refreshing state... [id=harbor] module.harbor.kubernetes_config_map_v1.harbor_portal_nginx: Refreshing state... [id=harbor/harbor-portal-nginx] module.harbor.kubernetes_config_map_v1.harbor_portal_css: Refreshing state... [id=harbor/harbor-portal-css] module.networking.kubernetes_ingress_v1.harbor_funnel: Refreshing state... [id=harbor/harbor-funnel] module.harbor.kubernetes_service_v1.harbor_portal_proxy: Refreshing state... [id=harbor/harbor-portal-proxy] module.harbor.helm_release.harbor: Refreshing state... [id=harbor] kubernetes_manifest.netpol_harbor: Refreshing state... module.storage.kubernetes_config_map_v1.minio_console_nginx: Refreshing state... [id=minio/minio-console-nginx] module.storage.helm_release.minio: Refreshing state... [id=minio] module.storage.kubernetes_config_map_v1.minio_console_css: Refreshing state... [id=minio/minio-console-css] module.storage.kubernetes_service_v1.minio_console_proxy: Refreshing state... [id=minio/minio-console-proxy] module.networking.kubernetes_ingress_v1.minio_api_funnel: Refreshing state... [id=minio/minio-api-funnel] module.networking.kubernetes_ingress_v1.minio_funnel: Refreshing state... [id=minio/minio-funnel] kubernetes_manifest.netpol_minio: Refreshing state... module.harbor.null_resource.harbor_oidc: Refreshing state... [id=2304382569774565098] module.harbor.kubernetes_deployment_v1.harbor_portal_proxy: Refreshing state... [id=harbor/harbor-portal-proxy] module.storage.minio_s3_bucket.assets: Refreshing state... [id=assets] module.storage.minio_iam_policy.cnpg_wal: Refreshing state... [id=cnpg-wal] module.storage.minio_iam_user.cnpg: Refreshing state... [id=cnpg] module.storage.minio_s3_bucket.tf_state_backups: Refreshing state... [id=tf-state-backups] module.storage.minio_iam_policy.tf_backup: Refreshing state... [id=tf-backup] module.storage.minio_iam_user.tf_backup: Refreshing state... [id=tf-backup] module.storage.minio_s3_bucket.postgres_wal: Refreshing state... [id=postgres-wal] module.database.kubernetes_secret_v1.cnpg_s3_creds: Refreshing state... [id=postgres/cnpg-s3-creds] module.storage.minio_iam_user_policy_attachment.cnpg: Refreshing state... [id=cnpg-20260302210642491000000001] module.storage.minio_s3_bucket_policy.assets_public_read: Refreshing state... [id=assets] module.storage.kubernetes_deployment_v1.minio_console_proxy: Refreshing state... [id=minio/minio-console-proxy] module.storage.minio_iam_user_policy_attachment.tf_backup: Refreshing state... [id=tf-backup-20260314163610110100000001] module.database.kubernetes_cron_job_v1.cnpg_backup_verify: Refreshing state... [id=postgres/cnpg-backup-verify] module.ops.kubernetes_namespace_v1.ollama: Refreshing state... [id=ollama] module.ops.data.kubernetes_namespace_v1.tofu_state: Reading... module.ops.kubernetes_service_v1.embedding_worker_metrics: Refreshing state... [id=pal-e-app/embedding-worker-metrics] module.ops.helm_release.nvidia_device_plugin: Refreshing state... [id=nvidia-device-plugin] module.ops.data.kubernetes_namespace_v1.tofu_state: Read complete after 0s [id=tofu-state] module.ops.kubernetes_role_v1.tf_backup: Refreshing state... [id=tofu-state/tf-state-backup] module.ops.kubernetes_service_account_v1.tf_backup: Refreshing state... [id=tofu-state/tf-state-backup] module.ops.kubernetes_secret_v1.tf_backup_s3_creds: Refreshing state... [id=tofu-state/tf-backup-s3-creds] module.ci.kubernetes_namespace_v1.woodpecker: Refreshing state... [id=woodpecker] module.ops.kubernetes_role_binding_v1.tf_backup: Refreshing state... [id=tofu-state/tf-state-backup] kubernetes_manifest.netpol_ollama: Refreshing state... module.ci.kubernetes_secret_v1.woodpecker_db_credentials: Refreshing state... [id=woodpecker/woodpecker-db-credentials] module.ci.kubernetes_secret_v1.woodpecker_cnpg_s3_creds: Refreshing state... [id=woodpecker/cnpg-s3-creds] module.ops.kubernetes_cron_job_v1.tf_state_backup: Refreshing state... [id=tofu-state/tf-state-backup] module.networking.kubernetes_ingress_v1.woodpecker_funnel: Refreshing state... [id=woodpecker/woodpecker-funnel] kubernetes_manifest.netpol_woodpecker: Refreshing state... module.ci.kubernetes_manifest.woodpecker_postgres: Refreshing state... module.ops.helm_release.ollama: Refreshing state... [id=ollama] module.ci.helm_release.woodpecker: Refreshing state... [id=woodpecker] module.ci.kubernetes_manifest.woodpecker_postgres_scheduled_backup: Refreshing state... module.ci.kubernetes_manifest.woodpecker_postgres_podmonitor: Refreshing state... OpenTofu used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create -/+ destroy and then create replacement OpenTofu will perform the following actions: # module.database.kubernetes_job_v1.admin_app_user_provision will be created + resource "kubernetes_job_v1" "admin_app_user_provision" { + id = (known after apply) + wait_for_completion = true + metadata { + generation = (known after apply) + labels = { + "app.kubernetes.io/managed-by" = "pal-e-platform-terraform" + "app.kubernetes.io/name" = "admin-app-user-provision" + "arch" = "postgres" + "story" = "admin-row-crud" } + name = (sensitive value) + namespace = "basketball-api" + resource_version = (known after apply) + uid = (known after apply) } + spec { + backoff_limit = 4 + completion_mode = (known after apply) + completions = 1 + parallelism = 1 + ttl_seconds_after_finished = "3600" + selector (known after apply) + template { + metadata { + generation = (known after apply) + labels = { + "app.kubernetes.io/name" = "admin-app-user-provision" } + name = (known after apply) + resource_version = (known after apply) + uid = (known after apply) } + spec { + automount_service_account_token = true + dns_policy = "ClusterFirst" + enable_service_links = true + host_ipc = false + host_network = false + host_pid = false + hostname = (known after apply) + node_name = (known after apply) + restart_policy = "OnFailure" + scheduler_name = (known after apply) + service_account_name = (known after apply) + share_process_namespace = false + termination_grace_period_seconds = 30 + container { + args = [ + <<-EOT set -euo pipefail echo "==> Provisioning admin_app role on ${PGHOST}/${PGDATABASE}" # Idempotent: CREATE ROLE if missing, otherwise rotate password. psql -v ON_ERROR_STOP=1 -v admin_pw="${ADMIN_APP_PASSWORD}" <<'SQL' -- Idempotent role creation/rotation. Conditional uses psql's \gset + \if -- (client-side) so we can use :'admin_pw' substitution. psql variable -- substitution does NOT work inside DO $$...$$ dollar-quoted blocks. SELECT EXISTS(SELECT 1 FROM pg_roles WHERE rolname = 'admin_app') AS role_exists \gset \if :role_exists ALTER ROLE admin_app WITH LOGIN PASSWORD :'admin_pw'; \else CREATE ROLE admin_app WITH LOGIN PASSWORD :'admin_pw'; \endif -- DML-only grants on schema public. Idempotent (re-grant is a no-op). GRANT USAGE ON SCHEMA public TO admin_app; GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO admin_app; GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO admin_app; -- Forward grants for tables/sequences created later by Drizzle migrations -- (which run as the basketball superuser). ALTER DEFAULT PRIVILEGES FOR ROLE basketball IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO admin_app; ALTER DEFAULT PRIVILEGES FOR ROLE basketball IN SCHEMA public GRANT USAGE ON SEQUENCES TO admin_app; SQL echo "==> admin_app role provisioned successfully" EOT, ] + command = [ + "/bin/sh", + "-c", ] + image = "postgres:16-alpine" + image_pull_policy = (known after apply) + name = "psql" + stdin = false + stdin_once = false + termination_message_path = "/dev/termination-log" + termination_message_policy = (known after apply) + tty = false + env { + name = "PGHOST" + value = "postgres.basketball-api.svc.cluster.local" } + env { + name = "PGPORT" + value = "5432" } + env { + name = "PGUSER" + value = "basketball" } + env { + name = "PGDATABASE" + value = "basketball" } + env { + name = "PGPASSWORD" + value_from { + secret_key_ref { + key = "postgres-password" + name = "basketball-api-secrets" } } } + env { + name = "ADMIN_APP_PASSWORD" + value = (sensitive value) } + resources { + limits = { + "memory" = "128Mi" } + requests = { + "cpu" = "50m" + "memory" = "64Mi" } } } + image_pull_secrets (known after apply) + readiness_gate (known after apply) } } } + timeouts { + create = "5m" + update = "5m" } } # module.harbor.null_resource.harbor_oidc is tainted, so it must be replaced -/+ resource "null_resource" "harbor_oidc" { ~ id = "2304382569774565098" -> (known after apply) # (1 unchanged attribute hidden) } Plan: 2 to add, 0 to change, 1 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 Review -- #361

Dockerfile (docker/ruby-arch/Dockerfile)

PASS -- clean and correct.

  • Package names are verified Arch packages (ruby, jemalloc, postgresql-libs, libyaml, pkgconf, base-devel)
  • Bundler installed via gem install bundler (correct -- avoids pacman version pinning mismatch)
  • Cache cleanup is thorough (pacman -Scc + rm -rf /var/cache/pacman/pkg/*)
  • LD_PRELOAD for jemalloc is set correctly
  • Multi-stage pattern is clean: base for runtime, build extends with dev tooling
  • No secrets, no hardcoded values

Pipeline (docker/ruby-arch/.woodpecker.yaml)

PASS with note -- Kaniko pattern matches sibling repos exactly.

  • Clone step matches basketball-api convention (no .netrc needed since no private repo access during build)
  • Kaniko settings (registry, insecure, extra_opts, from_secret) match basketball-api and pal-e-docs patterns exactly
  • dockerfile and context paths are correctly set for the nested location
  • Secrets use from_secret (no hardcoded credentials)
  • Event triggers correctly scoped to manual and cron

Note: The pipeline file lives at docker/ruby-arch/.woodpecker.yaml, not at the repo root or in .woodpecker/. Woodpecker will NOT auto-discover this file. This is fine -- the cron job in the Woodpecker UI must specify the pipeline config path (docker/ruby-arch/.woodpecker.yaml). This should be documented in the manual gates section, which it partially is.

SOP Compliance

  • Branch naming: 360-arch-ruby-base-image follows {issue-number}-{kebab-case-purpose}
  • PR body has all required sections (Summary, Changes, Test Plan, Review Checklist, Related Notes)
  • Closes #360 present for auto-close
  • No secrets committed
  • No unnecessary file changes (only 2 new files)
  • Commit message is descriptive

VERDICT: PASS

## PR Review -- #361 ### Dockerfile (`docker/ruby-arch/Dockerfile`) **PASS** -- clean and correct. - Package names are verified Arch packages (`ruby`, `jemalloc`, `postgresql-libs`, `libyaml`, `pkgconf`, `base-devel`) - Bundler installed via `gem install bundler` (correct -- avoids pacman version pinning mismatch) - Cache cleanup is thorough (`pacman -Scc` + `rm -rf /var/cache/pacman/pkg/*`) - `LD_PRELOAD` for jemalloc is set correctly - Multi-stage pattern is clean: `base` for runtime, `build` extends with dev tooling - No secrets, no hardcoded values ### Pipeline (`docker/ruby-arch/.woodpecker.yaml`) **PASS with note** -- Kaniko pattern matches sibling repos exactly. - Clone step matches basketball-api convention (no `.netrc` needed since no private repo access during build) - Kaniko settings (`registry`, `insecure`, `extra_opts`, `from_secret`) match basketball-api and pal-e-docs patterns exactly - `dockerfile` and `context` paths are correctly set for the nested location - Secrets use `from_secret` (no hardcoded credentials) - Event triggers correctly scoped to `manual` and `cron` **Note:** The pipeline file lives at `docker/ruby-arch/.woodpecker.yaml`, not at the repo root or in `.woodpecker/`. Woodpecker will NOT auto-discover this file. This is fine -- the cron job in the Woodpecker UI must specify the pipeline config path (`docker/ruby-arch/.woodpecker.yaml`). This should be documented in the manual gates section, which it partially is. ### SOP Compliance - [x] Branch naming: `360-arch-ruby-base-image` follows `{issue-number}-{kebab-case-purpose}` - [x] PR body has all required sections (Summary, Changes, Test Plan, Review Checklist, Related Notes) - [x] `Closes #360` present for auto-close - [x] No secrets committed - [x] No unnecessary file changes (only 2 new files) - [x] Commit message is descriptive ### VERDICT: PASS
ldraney deleted branch 360-arch-ruby-base-image 2026-05-10 16:47:22 +00:00
Sign in to join this conversation.
No description provided.