Add CI plan-on-PR and apply-on-merge pipeline (Phase 6.3+6.4) #50

Merged
forgejo_admin merged 4 commits from 48-ci-plan-and-apply-pipeline into main 2026-03-14 18:06:24 +00:00

Summary

Adds plan and apply steps to the Woodpecker CI pipeline. Plan runs on every PR (after validate passes) and posts tofu plan output as a Forgejo PR comment. Apply runs on push to main and executes tofu apply -auto-approve for automated infrastructure deployment on merge.

Changes

  • .woodpecker.yaml — Restructured from single top-level when to step-level when blocks. Added plan step (triggers on pull_request, depends on validate, posts plan output as PR comment via Forgejo API) and apply step (triggers on push to main only). Both steps inject all 17 Woodpecker secrets: kubeconfig_content, forgejo_token, and 15 tf_var_* secrets. Kubeconfig is written to /tmp/kubeconfig with chmod 600 and used via -backend-config="config_path=/tmp/kubeconfig" and TF_VAR_kubeconfig_path to override the hardcoded ~/.kube/config in versions.tf.

Test Plan

  • Open this PR and verify the validate step still passes
  • Verify the plan step runs after validate and posts plan output as a PR comment
  • After merge, verify the apply step triggers on the push-to-main event
  • Verify apply step completes successfully (no infrastructure drift expected)

Review Checklist

  • No .tf files changed, so tofu fmt / tofu validate not applicable
  • All 15 tf_var_* secrets match the variables defined in variables.tf
  • Kubeconfig override uses 10.0.0.217:6443 (node IP for CI pod network namespace)
  • Plan step posts comment before failing (so PR always gets plan output)
  • Apply step only triggers on push to main (not on PRs)
  • YAML validates (checked with Python yaml.safe_load)
  • Plan: plan-pal-e-platform (Phase 6.3 + 6.4)
  • Closes #48
  • Closes #49
## Summary Adds `plan` and `apply` steps to the Woodpecker CI pipeline. Plan runs on every PR (after validate passes) and posts tofu plan output as a Forgejo PR comment. Apply runs on push to main and executes `tofu apply -auto-approve` for automated infrastructure deployment on merge. ## Changes - `.woodpecker.yaml` — Restructured from single top-level `when` to step-level `when` blocks. Added `plan` step (triggers on `pull_request`, depends on `validate`, posts plan output as PR comment via Forgejo API) and `apply` step (triggers on `push` to `main` only). Both steps inject all 17 Woodpecker secrets: `kubeconfig_content`, `forgejo_token`, and 15 `tf_var_*` secrets. Kubeconfig is written to `/tmp/kubeconfig` with `chmod 600` and used via `-backend-config="config_path=/tmp/kubeconfig"` and `TF_VAR_kubeconfig_path` to override the hardcoded `~/.kube/config` in `versions.tf`. ## Test Plan - [ ] Open this PR and verify the `validate` step still passes - [ ] Verify the `plan` step runs after validate and posts plan output as a PR comment - [ ] After merge, verify the `apply` step triggers on the push-to-main event - [ ] Verify `apply` step completes successfully (no infrastructure drift expected) ## Review Checklist - [x] No .tf files changed, so `tofu fmt` / `tofu validate` not applicable - [x] All 15 `tf_var_*` secrets match the variables defined in `variables.tf` - [x] Kubeconfig override uses `10.0.0.217:6443` (node IP for CI pod network namespace) - [x] Plan step posts comment before failing (so PR always gets plan output) - [x] Apply step only triggers on push to main (not on PRs) - [x] YAML validates (checked with Python yaml.safe_load) ## Related - Plan: `plan-pal-e-platform` (Phase 6.3 + 6.4) - Closes #48 - Closes #49
Add two new steps to the Woodpecker CI pipeline:

- plan: runs on pull_request events after validate passes. Executes
  tofu plan with full secrets, posts plan output as a PR comment via
  Forgejo API, and fails the step if plan errors.

- apply: runs on push to main only. Executes tofu apply -auto-approve
  with full secrets for automated infrastructure deployment on merge.

Both steps write the kubeconfig secret to /tmp and override the backend
config_path and provider kubeconfig_path to work in CI's separate
network namespace (10.0.0.217:6443 instead of localhost).

Restructured top-level when block to step-level when blocks since
validate+plan trigger on pull_request while apply triggers on push.

Closes #48
Closes #49

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

Self-Review

Verdict: LGTM -- ready for human review.

Verification

  • All 15 tf_var_* secrets cross-referenced against variables.tf -- complete match, no missing or extra entries.
  • kubeconfig_path correctly set via export in commands (not a Woodpecker secret) since it points to /tmp/kubeconfig written at runtime.
  • forgejo_token included in plan step (needed for PR comment API), excluded from apply step (not needed). Correct.
  • kubeconfig_content included in both steps. Correct.
  • Step-level when blocks correctly replace the former top-level when. validate and plan trigger on pull_request; apply triggers on push to main.
  • depends_on: [validate] ensures plan only runs after validate passes.
  • Plan output posted via jq --rawfile + curl -d @file pattern avoids shell escaping issues with plan text.
  • Plan failure detection uses sentinel file (/tmp/plan-failed) to allow comment posting before step failure.
  • $$ escaping used correctly in multiline blocks for shell variables ($$PLAN_OUTPUT, $${CI_REPO}, etc.) to prevent Woodpecker envsubst from consuming them.
  • YAML validates with yaml.safe_load.

Notes

  • The validate step is unchanged in behavior (same commands, same image). Only structural change is moving when from top-level to step-level.
  • Alpine ash shell supports ${#VAR} (string length) and ${VAR:offset:length} (substring) used in the truncation logic.
## Self-Review **Verdict: LGTM -- ready for human review.** ### Verification - All 15 `tf_var_*` secrets cross-referenced against `variables.tf` -- complete match, no missing or extra entries. - `kubeconfig_path` correctly set via `export` in commands (not a Woodpecker secret) since it points to `/tmp/kubeconfig` written at runtime. - `forgejo_token` included in `plan` step (needed for PR comment API), excluded from `apply` step (not needed). Correct. - `kubeconfig_content` included in both steps. Correct. - Step-level `when` blocks correctly replace the former top-level `when`. `validate` and `plan` trigger on `pull_request`; `apply` triggers on `push` to `main`. - `depends_on: [validate]` ensures plan only runs after validate passes. - Plan output posted via `jq --rawfile` + `curl -d @file` pattern avoids shell escaping issues with plan text. - Plan failure detection uses sentinel file (`/tmp/plan-failed`) to allow comment posting before step failure. - `$$` escaping used correctly in multiline blocks for shell variables (`$$PLAN_OUTPUT`, `$${CI_REPO}`, etc.) to prevent Woodpecker envsubst from consuming them. - YAML validates with `yaml.safe_load`. ### Notes - The `validate` step is unchanged in behavior (same commands, same image). Only structural change is moving `when` from top-level to step-level. - Alpine `ash` shell supports `${#VAR}` (string length) and `${VAR:offset:length}` (substring) used in the truncation logic.
Author
Owner

PR #50 Review

BLOCKERS

1. $KUBECONFIG_CONTENT (single $) on lines 39 and 87 -- potential multiline breakage

Both the plan and apply steps use:

- echo "$KUBECONFIG_CONTENT" > /tmp/kubeconfig

KUBECONFIG_CONTENT is declared in secrets:, so Woodpecker performs YAML-level substitution of $KUBECONFIG_CONTENT before the shell ever sees it. For a multiline kubeconfig (which it almost certainly is -- YAML with clusters, contexts, users), this substitution injects raw newlines into the YAML command string, which will break parsing or produce a malformed echo command.

Every other variable reference in the multiline block correctly uses $$ (e.g., $$PLAN_OUTPUT, $$FORGEJO_TOKEN, $${CI_REPO}), which tells Woodpecker to emit a literal $ so the shell expands the env var instead. KUBECONFIG_CONTENT should follow the same pattern:

- echo "$$KUBECONFIG_CONTENT" > /tmp/kubeconfig

This applies to both line 39 (plan step) and line 87 (apply step).

2. Bashism in Alpine ash shell -- ${PLAN_OUTPUT:0:60000} substring syntax (lines 50-52)

The truncation logic uses bash substring expansion:

if [ ${#PLAN_OUTPUT} -gt 60000 ]; then
  PLAN_OUTPUT="${PLAN_OUTPUT:0:60000}...(truncated)"
fi

The ${var:offset:length} syntax is a bashism. The ghcr.io/opentofu/opentofu:1.9 image is Alpine-based and uses ash as /bin/sh. Woodpecker v3 runs commands via /bin/sh by default. This will produce a syntax error at runtime.

POSIX-compatible alternatives:

  • PLAN_OUTPUT=$(echo "$PLAN_OUTPUT" | head -c 60000) (uses head)
  • PLAN_OUTPUT=$(printf '%.60000s' "$PLAN_OUTPUT") (uses printf truncation)

The ${#PLAN_OUTPUT} length check on line 50 IS POSIX-compliant and will work in ash.

3. TF_VAR secret case mismatch -- high risk of silent variable injection failure

Woodpecker v3 exposes secrets as uppercased environment variables. The secret tf_var_tailscale_oauth_client_id becomes the env var TF_VAR_TAILSCALE_OAUTH_CLIENT_ID. OpenTofu matches TF_VAR_* environment variables case-sensitively against variable names. The variable is declared as:

variable "tailscale_oauth_client_id" { ... }

OpenTofu looks for TF_VAR_tailscale_oauth_client_id (lowercase after the prefix). It will NOT match TF_VAR_TAILSCALE_OAUTH_CLIENT_ID.

This affects all 15 tf_var_* secrets. The result would be silent failure: variables with defaults would use defaults, variables without defaults would error on tofu plan.

Recommended fix: Explicitly re-export each secret in lowercase in the commands: block:

commands:
  - export TF_VAR_tailscale_oauth_client_id="$$TF_VAR_TAILSCALE_OAUTH_CLIENT_ID"
  # ... for each variable

Or use a loop:

for var in tailscale_oauth_client_id tailscale_oauth_client_secret ...; do
  upper=$(echo "TF_VAR_$var" | tr '[:lower:]' '[:upper:]')
  eval "export TF_VAR_$var=\"\$(printenv $upper)\""
done

Important caveat: I am flagging this based on documented Woodpecker v3 behavior (secrets uppercased to env vars) and OpenTofu behavior (case-sensitive TF_VAR_ matching). If Woodpecker v3 has changed this behavior to preserve case, or if OpenTofu does case-insensitive matching, this is a non-issue. Recommend testing with a single variable first (e.g., add printenv | grep -i TF_VAR as a debug command to verify actual env var names before the tofu plan call). Do NOT leave the debug command in the final version as it would leak secret values to logs.

NITS

4. No tofu validate / tofu fmt -check on push-to-main (apply step)

The validate step only runs on event: pull_request. On push-to-main, only apply runs. This means the apply step has no pre-validation gate. The code was validated during the PR phase, so this is low risk, but adding validate to the push event (or making apply depend on a validate step that also triggers on push) would provide defense-in-depth.

5. No kubeconfig cleanup in plan/apply steps

Both steps write the kubeconfig to /tmp/kubeconfig but never remove it. Since Woodpecker K8s backend runs each step container independently and /tmp is container-local (not a shared volume), the file is destroyed when the container exits. This is fine in practice. Mentioning only for documentation clarity -- a trap 'rm -f /tmp/kubeconfig' EXIT would make the security posture explicit.

6. Plan output posted even on plan success -- consider adding a summary line

The PR comment will always contain the full plan output. For large plans, adding a summary line (e.g., "Plan: X to add, Y to change, Z to destroy" or "No changes") at the top of the comment would make PR reviews faster without needing to read the full plan.

7. apk add on every PR run (line 37)

The plan step installs curl and jq via apk add on every run. This adds network latency and a failure point (Alpine mirror availability). Consider using a custom image with these tools pre-installed, or caching the apk layer. Low priority since CI runs are infrequent.

8. Duplicate secrets blocks

The 15 tf_var_* secrets are listed identically in both plan and apply steps (lines 21-35 and 70-84). This is correct but creates maintenance burden -- adding a new TF variable requires updating both blocks. Woodpecker v3 does not support YAML anchors for deduplication, so this is unavoidable. Just noting it for awareness.

SOP COMPLIANCE

  • Branch named after issue -- 48-ci-plan-and-apply-pipeline references issue #48
  • PR body follows template -- Has ## Summary, ## Changes, ## Test Plan, ## Related
  • Related references plan slug -- References plan-pal-e-platform (Phase 6.3 + 6.4)
  • Closes references -- PR body includes Closes #48 and Closes #49
  • No secrets committed -- No .env files, credentials, or secret values in diff
  • Scope is clean -- Only .woodpecker.yaml changed, no scope creep
  • No .tf files changed -- tofu fmt / tofu validate not applicable (correctly noted in PR body)
  • Correctness -- 3 blockers identified (kubeconfig $ vs $$, ash bashism, TF_VAR case mismatch)

VERDICT: NOT APPROVED

Three blockers must be addressed before merge:

  1. Fix $KUBECONFIG_CONTENT to $$KUBECONFIG_CONTENT on lines 39 and 87
  2. Replace bash substring syntax ${PLAN_OUTPUT:0:60000} with POSIX-compatible alternative
  3. Verify or fix TF_VAR case mismatch (add printenv | grep -i TF_VAR as a temporary debug line to confirm Woodpecker's actual behavior, then add re-export commands if needed)
## PR #50 Review ### BLOCKERS **1. `$KUBECONFIG_CONTENT` (single `$`) on lines 39 and 87 -- potential multiline breakage** Both the `plan` and `apply` steps use: ``` - echo "$KUBECONFIG_CONTENT" > /tmp/kubeconfig ``` `KUBECONFIG_CONTENT` is declared in `secrets:`, so Woodpecker performs YAML-level substitution of `$KUBECONFIG_CONTENT` *before* the shell ever sees it. For a multiline kubeconfig (which it almost certainly is -- YAML with clusters, contexts, users), this substitution injects raw newlines into the YAML command string, which will break parsing or produce a malformed `echo` command. Every other variable reference in the multiline block correctly uses `$$` (e.g., `$$PLAN_OUTPUT`, `$$FORGEJO_TOKEN`, `$${CI_REPO}`), which tells Woodpecker to emit a literal `$` so the *shell* expands the env var instead. `KUBECONFIG_CONTENT` should follow the same pattern: ``` - echo "$$KUBECONFIG_CONTENT" > /tmp/kubeconfig ``` This applies to both line 39 (plan step) and line 87 (apply step). **2. Bashism in Alpine `ash` shell -- `${PLAN_OUTPUT:0:60000}` substring syntax (lines 50-52)** The truncation logic uses bash substring expansion: ```sh if [ ${#PLAN_OUTPUT} -gt 60000 ]; then PLAN_OUTPUT="${PLAN_OUTPUT:0:60000}...(truncated)" fi ``` The `${var:offset:length}` syntax is a bashism. The `ghcr.io/opentofu/opentofu:1.9` image is Alpine-based and uses `ash` as `/bin/sh`. Woodpecker v3 runs commands via `/bin/sh` by default. This will produce a syntax error at runtime. POSIX-compatible alternatives: - `PLAN_OUTPUT=$(echo "$PLAN_OUTPUT" | head -c 60000)` (uses `head`) - `PLAN_OUTPUT=$(printf '%.60000s' "$PLAN_OUTPUT")` (uses `printf` truncation) The `${#PLAN_OUTPUT}` length check on line 50 IS POSIX-compliant and will work in `ash`. **3. TF_VAR secret case mismatch -- high risk of silent variable injection failure** Woodpecker v3 exposes secrets as uppercased environment variables. The secret `tf_var_tailscale_oauth_client_id` becomes the env var `TF_VAR_TAILSCALE_OAUTH_CLIENT_ID`. OpenTofu matches `TF_VAR_*` environment variables case-sensitively against variable names. The variable is declared as: ```hcl variable "tailscale_oauth_client_id" { ... } ``` OpenTofu looks for `TF_VAR_tailscale_oauth_client_id` (lowercase after the prefix). It will NOT match `TF_VAR_TAILSCALE_OAUTH_CLIENT_ID`. This affects all 15 `tf_var_*` secrets. The result would be silent failure: variables with defaults would use defaults, variables without defaults would error on `tofu plan`. **Recommended fix:** Explicitly re-export each secret in lowercase in the `commands:` block: ```yaml commands: - export TF_VAR_tailscale_oauth_client_id="$$TF_VAR_TAILSCALE_OAUTH_CLIENT_ID" # ... for each variable ``` Or use a loop: ```sh for var in tailscale_oauth_client_id tailscale_oauth_client_secret ...; do upper=$(echo "TF_VAR_$var" | tr '[:lower:]' '[:upper:]') eval "export TF_VAR_$var=\"\$(printenv $upper)\"" done ``` **Important caveat:** I am flagging this based on documented Woodpecker v3 behavior (secrets uppercased to env vars) and OpenTofu behavior (case-sensitive TF_VAR_ matching). If Woodpecker v3 has changed this behavior to preserve case, or if OpenTofu does case-insensitive matching, this is a non-issue. Recommend testing with a single variable first (e.g., add `printenv | grep -i TF_VAR` as a debug command to verify actual env var names before the `tofu plan` call). Do NOT leave the debug command in the final version as it would leak secret values to logs. ### NITS **4. No `tofu validate` / `tofu fmt -check` on push-to-main (apply step)** The `validate` step only runs on `event: pull_request`. On push-to-main, only `apply` runs. This means the apply step has no pre-validation gate. The code was validated during the PR phase, so this is low risk, but adding `validate` to the push event (or making `apply` depend on a validate step that also triggers on push) would provide defense-in-depth. **5. No kubeconfig cleanup in plan/apply steps** Both steps write the kubeconfig to `/tmp/kubeconfig` but never remove it. Since Woodpecker K8s backend runs each step container independently and `/tmp` is container-local (not a shared volume), the file is destroyed when the container exits. This is fine in practice. Mentioning only for documentation clarity -- a `trap 'rm -f /tmp/kubeconfig' EXIT` would make the security posture explicit. **6. Plan output posted even on plan success -- consider adding a summary line** The PR comment will always contain the full plan output. For large plans, adding a summary line (e.g., "Plan: X to add, Y to change, Z to destroy" or "No changes") at the top of the comment would make PR reviews faster without needing to read the full plan. **7. `apk add` on every PR run (line 37)** The `plan` step installs `curl` and `jq` via `apk add` on every run. This adds network latency and a failure point (Alpine mirror availability). Consider using a custom image with these tools pre-installed, or caching the apk layer. Low priority since CI runs are infrequent. **8. Duplicate secrets blocks** The 15 `tf_var_*` secrets are listed identically in both `plan` and `apply` steps (lines 21-35 and 70-84). This is correct but creates maintenance burden -- adding a new TF variable requires updating both blocks. Woodpecker v3 does not support YAML anchors for deduplication, so this is unavoidable. Just noting it for awareness. ### SOP COMPLIANCE - [x] **Branch named after issue** -- `48-ci-plan-and-apply-pipeline` references issue #48 - [x] **PR body follows template** -- Has ## Summary, ## Changes, ## Test Plan, ## Related - [x] **Related references plan slug** -- References `plan-pal-e-platform` (Phase 6.3 + 6.4) - [x] **Closes references** -- PR body includes `Closes #48` and `Closes #49` - [x] **No secrets committed** -- No .env files, credentials, or secret values in diff - [x] **Scope is clean** -- Only `.woodpecker.yaml` changed, no scope creep - [x] **No .tf files changed** -- `tofu fmt` / `tofu validate` not applicable (correctly noted in PR body) - [ ] **Correctness** -- 3 blockers identified (kubeconfig `$` vs `$$`, ash bashism, TF_VAR case mismatch) ### VERDICT: NOT APPROVED Three blockers must be addressed before merge: 1. Fix `$KUBECONFIG_CONTENT` to `$$KUBECONFIG_CONTENT` on lines 39 and 87 2. Replace bash substring syntax `${PLAN_OUTPUT:0:60000}` with POSIX-compatible alternative 3. Verify or fix TF_VAR case mismatch (add `printenv | grep -i TF_VAR` as a temporary debug line to confirm Woodpecker's actual behavior, then add re-export commands if needed)
Fix Woodpecker v3 secrets syntax: use environment+from_secret
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
e696c3778e
Woodpecker v3 removed the `secrets:` step property. Replace with
`environment:` + `from_secret:` syntax on both plan and apply steps.

TF_VAR env var names use lowercase after the prefix (e.g.
TF_VAR_grafana_admin_password) to match OpenTofu variable names
exactly, since TF_VAR matching is case-sensitive on Linux.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add CI trigger comment to force fresh pipeline run
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
15ef351347
Woodpecker cannot re-fetch PR config on restart, so pushing a no-op
change to trigger a new pipeline with the corrected secrets syntax.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix PR comment API endpoint: use issues/ not pulls/
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
aa99ba8102
Forgejo's API routes PR comments through the issues endpoint.
POST pulls/{N}/comments returns 405; POST issues/{N}/comments
is the correct path.

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

Tofu Plan Output

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

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:

  # helm_release.kube_prometheus_stack will be updated in-place
  ~ resource "helm_release" "kube_prometheus_stack" {
        id                         = "kube-prometheus-stack"
        name                       = "kube-prometheus-stack"
      ~ values                     = [
          # Warning: this attribute value will no longer be marked as sensitive
          # after applying this change. The value is unchanged.
          ~ (sensitive value),
        ]
        # (28 unchanged attributes hidden)

        # (3 unchanged blocks 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 ``` data.kubernetes_namespace_v1.pal_e_docs: Reading... tailscale_acl.this: Refreshing state... [id=acl] kubernetes_namespace_v1.woodpecker: Refreshing state... [id=woodpecker] helm_release.nvidia_device_plugin: Refreshing state... [id=nvidia-device-plugin] kubernetes_namespace_v1.ollama: Refreshing state... [id=ollama] data.kubernetes_namespace_v1.tofu_state: Reading... kubernetes_namespace_v1.keycloak: Refreshing state... [id=keycloak] kubernetes_namespace_v1.postgres: Refreshing state... [id=postgres] kubernetes_namespace_v1.harbor: Refreshing state... [id=harbor] kubernetes_namespace_v1.minio: Refreshing state... [id=minio] data.kubernetes_namespace_v1.tofu_state: Read complete after 0s [id=tofu-state] data.kubernetes_namespace_v1.pal_e_docs: Read complete after 0s [id=pal-e-docs] kubernetes_namespace_v1.forgejo: Refreshing state... [id=forgejo] kubernetes_namespace_v1.cnpg_system: Refreshing state... [id=cnpg-system] kubernetes_namespace_v1.tailscale: Refreshing state... [id=tailscale] kubernetes_namespace_v1.monitoring: Refreshing state... [id=monitoring] kubernetes_service_account_v1.tf_backup: Refreshing state... [id=tofu-state/tf-state-backup] kubernetes_secret_v1.paledocs_db_url: Refreshing state... [id=pal-e-docs/paledocs-db-url] kubernetes_role_v1.tf_backup: Refreshing state... [id=tofu-state/tf-state-backup] kubernetes_persistent_volume_claim_v1.keycloak_data: Refreshing state... [id=keycloak/keycloak-data] kubernetes_secret_v1.keycloak_admin: Refreshing state... [id=keycloak/keycloak-admin] kubernetes_service_v1.keycloak: Refreshing state... [id=keycloak/keycloak] helm_release.cnpg: Refreshing state... [id=cnpg] helm_release.forgejo: Refreshing state... [id=forgejo] kubernetes_service_v1.dora_exporter: Refreshing state... [id=monitoring/dora-exporter] helm_release.tailscale_operator: Refreshing state... [id=tailscale-operator] kubernetes_secret_v1.dora_exporter: Refreshing state... [id=monitoring/dora-exporter] helm_release.loki_stack: Refreshing state... [id=loki-stack] kubernetes_role_binding_v1.tf_backup: Refreshing state... [id=tofu-state/tf-state-backup] helm_release.kube_prometheus_stack: Refreshing state... [id=kube-prometheus-stack] kubernetes_deployment_v1.keycloak: Refreshing state... [id=keycloak/keycloak] helm_release.ollama: Refreshing state... [id=ollama] helm_release.woodpecker: Refreshing state... [id=woodpecker] kubernetes_ingress_v1.forgejo_funnel: Refreshing state... [id=forgejo/forgejo-funnel] kubernetes_ingress_v1.keycloak_funnel: Refreshing state... [id=keycloak/keycloak-funnel] kubernetes_config_map_v1.grafana_loki_datasource: Refreshing state... [id=monitoring/grafana-loki-datasource] kubernetes_ingress_v1.alertmanager_funnel: Refreshing state... [id=monitoring/alertmanager-funnel] kubernetes_ingress_v1.grafana_funnel: Refreshing state... [id=monitoring/grafana-funnel] kubernetes_config_map_v1.dora_dashboard: Refreshing state... [id=monitoring/dora-dashboard] helm_release.harbor: Refreshing state... [id=harbor] helm_release.minio: Refreshing state... [id=minio] kubernetes_deployment_v1.dora_exporter: Refreshing state... [id=monitoring/dora-exporter] kubernetes_manifest.dora_exporter_service_monitor: Refreshing state... kubernetes_ingress_v1.woodpecker_funnel: Refreshing state... [id=woodpecker/woodpecker-funnel] minio_iam_policy.tf_backup: Refreshing state... [id=tf-backup] minio_s3_bucket.postgres_wal: Refreshing state... [id=postgres-wal] minio_s3_bucket.assets: Refreshing state... [id=assets] minio_iam_policy.cnpg_wal: Refreshing state... [id=cnpg-wal] minio_s3_bucket.tf_state_backups: Refreshing state... [id=tf-state-backups] minio_iam_user.tf_backup: Refreshing state... [id=tf-backup] minio_iam_user.cnpg: Refreshing state... [id=cnpg] kubernetes_ingress_v1.minio_funnel: Refreshing state... [id=minio/minio-funnel] kubernetes_ingress_v1.minio_api_funnel: Refreshing state... [id=minio/minio-api-funnel] 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.cnpg_s3_creds: Refreshing state... [id=postgres/cnpg-s3-creds] kubernetes_secret_v1.tf_backup_s3_creds: Refreshing state... [id=tofu-state/tf-backup-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] 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: # helm_release.kube_prometheus_stack will be updated in-place ~ resource "helm_release" "kube_prometheus_stack" { id = "kube-prometheus-stack" name = "kube-prometheus-stack" ~ values = [ # Warning: this attribute value will no longer be marked as sensitive # after applying this change. The value is unchanged. ~ (sensitive value), ] # (28 unchanged attributes hidden) # (3 unchanged blocks 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. ```
forgejo_admin deleted branch 48-ci-plan-and-apply-pipeline 2026-03-14 18:06:24 +00:00
Sign in to join this conversation.
No description provided.