Initial Commit
Some checks failed
Deploy / Update K8s Apps / Detect changed K8s tfvars (push) Failing after 13s
Deploy / Update Apps / Detect changed tfvars files (push) Failing after 13s
Test / Static Analysis (push) Failing after 11s
Test / Unit Tests — Docker Stack (push) Has been skipped
Test / Unit Tests — K8s Stack (push) Has been skipped
Deploy / Update K8s Apps / Deploy ${{ matrix.tfvars }} (push) Has been skipped
Deploy / Update K8s Apps / Destroy ${{ matrix.tfvars }} (push) Has been skipped
Deploy / Update Apps / Deploy ${{ matrix.tfvars }} (push) Has been skipped
Deploy / Update Apps / Destroy ${{ matrix.tfvars }} (push) Has been skipped
Test / Integration Test — K8s (k3d) (push) Has been skipped
Some checks failed
Deploy / Update K8s Apps / Detect changed K8s tfvars (push) Failing after 13s
Deploy / Update Apps / Detect changed tfvars files (push) Failing after 13s
Test / Static Analysis (push) Failing after 11s
Test / Unit Tests — Docker Stack (push) Has been skipped
Test / Unit Tests — K8s Stack (push) Has been skipped
Deploy / Update K8s Apps / Deploy ${{ matrix.tfvars }} (push) Has been skipped
Deploy / Update K8s Apps / Destroy ${{ matrix.tfvars }} (push) Has been skipped
Deploy / Update Apps / Deploy ${{ matrix.tfvars }} (push) Has been skipped
Deploy / Update Apps / Destroy ${{ matrix.tfvars }} (push) Has been skipped
Test / Integration Test — K8s (k3d) (push) Has been skipped
This commit is contained in:
215
.gitea/workflows/deploy-k8s.yml
Normal file
215
.gitea/workflows/deploy-k8s.yml
Normal file
@@ -0,0 +1,215 @@
|
||||
name: Deploy / Update K8s Apps
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "k8s/apps/**.tfvars"
|
||||
|
||||
env:
|
||||
TOFU_VERSION: "1.9.0"
|
||||
TOFU_WORKING_DIR: "k8s"
|
||||
|
||||
jobs:
|
||||
detect-changes:
|
||||
name: Detect changed K8s tfvars
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
added_modified: ${{ steps.diff.outputs.added_modified }}
|
||||
deleted: ${{ steps.diff.outputs.deleted }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Compute diff
|
||||
id: diff
|
||||
run: |
|
||||
ADDED_MODIFIED=$(git diff --name-only --diff-filter=ACM HEAD~1 HEAD -- 'k8s/apps/*.tfvars' | jq -R -s -c 'split("\n") | map(select(length > 0))')
|
||||
DELETED=$(git diff --name-only --diff-filter=D HEAD~1 HEAD -- 'k8s/apps/*.tfvars' | jq -R -s -c 'split("\n") | map(select(length > 0))')
|
||||
echo "added_modified=$ADDED_MODIFIED" >> "$GITHUB_OUTPUT"
|
||||
echo "deleted=$DELETED" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# ─── Deploy / Update ──────────────────────────────────────────────────────
|
||||
deploy:
|
||||
name: Deploy ${{ matrix.tfvars }}
|
||||
needs: detect-changes
|
||||
if: ${{ needs.detect-changes.outputs.added_modified != '[]' }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
tfvars: ${{ fromJson(needs.detect-changes.outputs.added_modified) }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install OpenTofu
|
||||
run: |
|
||||
curl -fsSL https://get.opentofu.org/install-opentofu.sh | sh -s -- --install-method=standalone --opentofu-version=${{ env.TOFU_VERSION }}
|
||||
|
||||
- name: Install gopass
|
||||
run: |
|
||||
curl -fsSL https://github.com/gopasspw/gopass/releases/latest/download/gopass-linux-amd64.tar.gz | tar xz
|
||||
sudo mv gopass /usr/local/bin/
|
||||
|
||||
- name: Configure gopass
|
||||
env:
|
||||
GOPASS_GPG_KEY: ${{ secrets.GOPASS_GPG_KEY }}
|
||||
GOPASS_STORE_REPO: ${{ secrets.GOPASS_STORE_REPO }}
|
||||
run: |
|
||||
echo "$GOPASS_GPG_KEY" | gpg --batch --import
|
||||
gopass clone "$GOPASS_STORE_REPO"
|
||||
|
||||
- name: Resolve app name from tfvars filename
|
||||
id: app
|
||||
run: |
|
||||
APP=$(basename "${{ matrix.tfvars }}" .tfvars)
|
||||
echo "name=$APP" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Fetch kubeconfig from gopass
|
||||
run: |
|
||||
# Adjust the gopass path to match your store layout.
|
||||
# The cluster name can be embedded in the tfvars or derived from the app name.
|
||||
gopass show -o "infra/kubeconfigs/${{ steps.app.outputs.name }}" > /tmp/kubeconfig
|
||||
chmod 600 /tmp/kubeconfig
|
||||
|
||||
- name: Fetch secrets from gopass
|
||||
id: secrets
|
||||
run: |
|
||||
RABBITMQ_PASSWORD=$(gopass show -o "apps/${{ steps.app.outputs.name }}/rabbitmq_password" 2>/dev/null || echo "")
|
||||
LOKI_AUTH_TOKEN=$(gopass show -o "apps/${{ steps.app.outputs.name }}/loki_token" 2>/dev/null || echo "")
|
||||
echo "::add-mask::$RABBITMQ_PASSWORD"
|
||||
echo "::add-mask::$LOKI_AUTH_TOKEN"
|
||||
echo "rabbitmq_password=$RABBITMQ_PASSWORD" >> "$GITHUB_OUTPUT"
|
||||
echo "loki_auth_token=$LOKI_AUTH_TOKEN" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: tofu init
|
||||
working-directory: ${{ env.TOFU_WORKING_DIR }}
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.SEAWEED_ACCESS_KEY }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SEAWEED_SECRET_KEY }}
|
||||
run: |
|
||||
tofu init \
|
||||
-backend-config="bucket=tofu-state" \
|
||||
-backend-config="key=apps-k8s/${{ steps.app.outputs.name }}.tfstate" \
|
||||
-backend-config="endpoint=${{ secrets.SEAWEED_S3_ENDPOINT }}" \
|
||||
-backend-config="region=us-east-1" \
|
||||
-backend-config="force_path_style=true"
|
||||
|
||||
- name: Select or create workspace
|
||||
working-directory: ${{ env.TOFU_WORKING_DIR }}
|
||||
run: |
|
||||
tofu workspace select "${{ steps.app.outputs.name }}" \
|
||||
|| tofu workspace new "${{ steps.app.outputs.name }}"
|
||||
|
||||
- name: tofu apply
|
||||
working-directory: ${{ env.TOFU_WORKING_DIR }}
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.SEAWEED_ACCESS_KEY }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SEAWEED_SECRET_KEY }}
|
||||
run: |
|
||||
tofu apply -auto-approve \
|
||||
-var-file="../${{ matrix.tfvars }}" \
|
||||
-var="kubeconfig_path=/tmp/kubeconfig" \
|
||||
-var="rabbitmq_password=${{ steps.secrets.outputs.rabbitmq_password }}" \
|
||||
-var="loki_auth_token=${{ steps.secrets.outputs.loki_auth_token }}"
|
||||
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
run: rm -f /tmp/kubeconfig
|
||||
|
||||
# ─── Destroy (tfvars file deleted) ────────────────────────────────────────
|
||||
destroy:
|
||||
name: Destroy ${{ matrix.tfvars }}
|
||||
needs: detect-changes
|
||||
if: ${{ needs.detect-changes.outputs.deleted != '[]' }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
tfvars: ${{ fromJson(needs.detect-changes.outputs.deleted) }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# Checkout previous commit to recover the deleted tfvars file
|
||||
ref: ${{ github.event.before }}
|
||||
|
||||
- name: Install OpenTofu
|
||||
run: |
|
||||
curl -fsSL https://get.opentofu.org/install-opentofu.sh | sh -s -- --install-method=standalone --opentofu-version=${{ env.TOFU_VERSION }}
|
||||
|
||||
- name: Install gopass
|
||||
run: |
|
||||
curl -fsSL https://github.com/gopasspw/gopass/releases/latest/download/gopass-linux-amd64.tar.gz | tar xz
|
||||
sudo mv gopass /usr/local/bin/
|
||||
|
||||
- name: Configure gopass
|
||||
env:
|
||||
GOPASS_GPG_KEY: ${{ secrets.GOPASS_GPG_KEY }}
|
||||
GOPASS_STORE_REPO: ${{ secrets.GOPASS_STORE_REPO }}
|
||||
run: |
|
||||
echo "$GOPASS_GPG_KEY" | gpg --batch --import
|
||||
gopass clone "$GOPASS_STORE_REPO"
|
||||
|
||||
- name: Resolve app name
|
||||
id: app
|
||||
run: |
|
||||
APP=$(basename "${{ matrix.tfvars }}" .tfvars)
|
||||
echo "name=$APP" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Fetch kubeconfig from gopass
|
||||
run: |
|
||||
gopass show -o "infra/kubeconfigs/${{ steps.app.outputs.name }}" > /tmp/kubeconfig
|
||||
chmod 600 /tmp/kubeconfig
|
||||
|
||||
- name: Fetch secrets from gopass
|
||||
id: secrets
|
||||
run: |
|
||||
RABBITMQ_PASSWORD=$(gopass show -o "apps/${{ steps.app.outputs.name }}/rabbitmq_password" 2>/dev/null || echo "")
|
||||
LOKI_AUTH_TOKEN=$(gopass show -o "apps/${{ steps.app.outputs.name }}/loki_token" 2>/dev/null || echo "")
|
||||
echo "::add-mask::$RABBITMQ_PASSWORD"
|
||||
echo "::add-mask::$LOKI_AUTH_TOKEN"
|
||||
echo "rabbitmq_password=$RABBITMQ_PASSWORD" >> "$GITHUB_OUTPUT"
|
||||
echo "loki_auth_token=$LOKI_AUTH_TOKEN" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: tofu init
|
||||
working-directory: ${{ env.TOFU_WORKING_DIR }}
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.SEAWEED_ACCESS_KEY }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SEAWEED_SECRET_KEY }}
|
||||
run: |
|
||||
tofu init \
|
||||
-backend-config="bucket=tofu-state" \
|
||||
-backend-config="key=apps-k8s/${{ steps.app.outputs.name }}.tfstate" \
|
||||
-backend-config="endpoint=${{ secrets.SEAWEED_S3_ENDPOINT }}" \
|
||||
-backend-config="region=us-east-1" \
|
||||
-backend-config="force_path_style=true"
|
||||
|
||||
- name: Select workspace
|
||||
working-directory: ${{ env.TOFU_WORKING_DIR }}
|
||||
run: tofu workspace select "${{ steps.app.outputs.name }}"
|
||||
|
||||
- name: tofu destroy
|
||||
working-directory: ${{ env.TOFU_WORKING_DIR }}
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.SEAWEED_ACCESS_KEY }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SEAWEED_SECRET_KEY }}
|
||||
run: |
|
||||
tofu destroy -auto-approve \
|
||||
-var-file="../${{ matrix.tfvars }}" \
|
||||
-var="kubeconfig_path=/tmp/kubeconfig" \
|
||||
-var="rabbitmq_password=${{ steps.secrets.outputs.rabbitmq_password }}" \
|
||||
-var="loki_auth_token=${{ steps.secrets.outputs.loki_auth_token }}"
|
||||
|
||||
- name: Delete workspace
|
||||
working-directory: ${{ env.TOFU_WORKING_DIR }}
|
||||
run: |
|
||||
tofu workspace select default
|
||||
tofu workspace delete "${{ steps.app.outputs.name }}"
|
||||
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
run: rm -f /tmp/kubeconfig
|
||||
208
.gitea/workflows/deploy.yml
Normal file
208
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,208 @@
|
||||
name: Deploy / Update Apps
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "apps/**.tfvars"
|
||||
|
||||
env:
|
||||
TOFU_VERSION: "1.9.0"
|
||||
|
||||
jobs:
|
||||
detect-changes:
|
||||
name: Detect changed tfvars files
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
added_modified: ${{ steps.diff.outputs.added_modified }}
|
||||
deleted: ${{ steps.diff.outputs.deleted }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Compute diff
|
||||
id: diff
|
||||
run: |
|
||||
ADDED_MODIFIED=$(git diff --name-only --diff-filter=ACM HEAD~1 HEAD -- 'apps/*.tfvars' | jq -R -s -c 'split("\n") | map(select(length > 0))')
|
||||
DELETED=$(git diff --name-only --diff-filter=D HEAD~1 HEAD -- 'apps/*.tfvars' | jq -R -s -c 'split("\n") | map(select(length > 0))')
|
||||
echo "added_modified=$ADDED_MODIFIED" >> "$GITHUB_OUTPUT"
|
||||
echo "deleted=$DELETED" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# ─── Deploy / Update ──────────────────────────────────────────────────────
|
||||
deploy:
|
||||
name: Deploy ${{ matrix.tfvars }}
|
||||
needs: detect-changes
|
||||
if: ${{ needs.detect-changes.outputs.added_modified != '[]' }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
tfvars: ${{ fromJson(needs.detect-changes.outputs.added_modified) }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install OpenTofu
|
||||
run: |
|
||||
curl -fsSL https://get.opentofu.org/install-opentofu.sh | sh -s -- --install-method=standalone --opentofu-version=${{ env.TOFU_VERSION }}
|
||||
|
||||
- name: Install gopass
|
||||
run: |
|
||||
curl -fsSL https://github.com/gopasspw/gopass/releases/latest/download/gopass-linux-amd64.tar.gz | tar xz
|
||||
sudo mv gopass /usr/local/bin/
|
||||
|
||||
- name: Configure gopass
|
||||
env:
|
||||
GOPASS_GPG_KEY: ${{ secrets.GOPASS_GPG_KEY }}
|
||||
GOPASS_STORE_REPO: ${{ secrets.GOPASS_STORE_REPO }}
|
||||
run: |
|
||||
echo "$GOPASS_GPG_KEY" | gpg --batch --import
|
||||
gopass clone "$GOPASS_STORE_REPO"
|
||||
|
||||
- name: Resolve app name from tfvars filename
|
||||
id: app
|
||||
run: |
|
||||
TFVARS="${{ matrix.tfvars }}"
|
||||
APP=$(basename "$TFVARS" .tfvars)
|
||||
echo "name=$APP" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Fetch SSH key from gopass
|
||||
run: |
|
||||
# Adjust the gopass path to match your store layout
|
||||
gopass show -o "infra/ssh-keys/${{ steps.app.outputs.name }}" > /tmp/deploy_key
|
||||
chmod 600 /tmp/deploy_key
|
||||
|
||||
- name: Fetch secrets from gopass
|
||||
id: secrets
|
||||
run: |
|
||||
DB_PASSWORD=$(gopass show -o "apps/${{ steps.app.outputs.name }}/db_password")
|
||||
GIT_TOKEN=$(gopass show -o "apps/${{ steps.app.outputs.name }}/git_token" 2>/dev/null || echo "")
|
||||
echo "::add-mask::$DB_PASSWORD"
|
||||
echo "::add-mask::$GIT_TOKEN"
|
||||
echo "db_password=$DB_PASSWORD" >> "$GITHUB_OUTPUT"
|
||||
echo "git_token=$GIT_TOKEN" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: tofu init
|
||||
env:
|
||||
# SeaweedFS S3 credentials (set in Gitea repo secrets)
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.SEAWEED_ACCESS_KEY }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SEAWEED_SECRET_KEY }}
|
||||
run: |
|
||||
tofu init \
|
||||
-backend-config="bucket=tofu-state" \
|
||||
-backend-config="key=apps/${{ steps.app.outputs.name }}.tfstate" \
|
||||
-backend-config="endpoint=${{ secrets.SEAWEED_S3_ENDPOINT }}" \
|
||||
-backend-config="region=us-east-1" \
|
||||
-backend-config="force_path_style=true"
|
||||
|
||||
- name: Select or create workspace
|
||||
run: |
|
||||
tofu workspace select "${{ steps.app.outputs.name }}" \
|
||||
|| tofu workspace new "${{ steps.app.outputs.name }}"
|
||||
|
||||
- name: tofu apply
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.SEAWEED_ACCESS_KEY }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SEAWEED_SECRET_KEY }}
|
||||
run: |
|
||||
tofu apply -auto-approve \
|
||||
-var-file="${{ matrix.tfvars }}" \
|
||||
-var="ssh_key_path=/tmp/deploy_key" \
|
||||
-var="db_password=${{ steps.secrets.outputs.db_password }}" \
|
||||
-var="openresty_git_token=${{ steps.secrets.outputs.git_token }}"
|
||||
|
||||
- name: Cleanup SSH key
|
||||
if: always()
|
||||
run: rm -f /tmp/deploy_key
|
||||
|
||||
# ─── Destroy (tfvars file deleted) ────────────────────────────────────────
|
||||
destroy:
|
||||
name: Destroy ${{ matrix.tfvars }}
|
||||
needs: detect-changes
|
||||
if: ${{ needs.detect-changes.outputs.deleted != '[]' }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
tfvars: ${{ fromJson(needs.detect-changes.outputs.deleted) }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# Checkout the previous commit so we still have the tfvars file
|
||||
ref: ${{ github.event.before }}
|
||||
|
||||
- name: Install OpenTofu
|
||||
run: |
|
||||
curl -fsSL https://get.opentofu.org/install-opentofu.sh | sh -s -- --install-method=standalone --opentofu-version=${{ env.TOFU_VERSION }}
|
||||
|
||||
- name: Install gopass
|
||||
run: |
|
||||
curl -fsSL https://github.com/gopasspw/gopass/releases/latest/download/gopass-linux-amd64.tar.gz | tar xz
|
||||
sudo mv gopass /usr/local/bin/
|
||||
|
||||
- name: Configure gopass
|
||||
env:
|
||||
GOPASS_GPG_KEY: ${{ secrets.GOPASS_GPG_KEY }}
|
||||
GOPASS_STORE_REPO: ${{ secrets.GOPASS_STORE_REPO }}
|
||||
run: |
|
||||
echo "$GOPASS_GPG_KEY" | gpg --batch --import
|
||||
gopass clone "$GOPASS_STORE_REPO"
|
||||
|
||||
- name: Resolve app name
|
||||
id: app
|
||||
run: |
|
||||
APP=$(basename "${{ matrix.tfvars }}" .tfvars)
|
||||
echo "name=$APP" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Fetch SSH key from gopass
|
||||
run: |
|
||||
gopass show -o "infra/ssh-keys/${{ steps.app.outputs.name }}" > /tmp/deploy_key
|
||||
chmod 600 /tmp/deploy_key
|
||||
|
||||
- name: Fetch secrets from gopass
|
||||
id: secrets
|
||||
run: |
|
||||
DB_PASSWORD=$(gopass show -o "apps/${{ steps.app.outputs.name }}/db_password")
|
||||
GIT_TOKEN=$(gopass show -o "apps/${{ steps.app.outputs.name }}/git_token" 2>/dev/null || echo "")
|
||||
echo "::add-mask::$DB_PASSWORD"
|
||||
echo "::add-mask::$GIT_TOKEN"
|
||||
echo "db_password=$DB_PASSWORD" >> "$GITHUB_OUTPUT"
|
||||
echo "git_token=$GIT_TOKEN" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: tofu init
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.SEAWEED_ACCESS_KEY }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SEAWEED_SECRET_KEY }}
|
||||
run: |
|
||||
tofu init \
|
||||
-backend-config="bucket=tofu-state" \
|
||||
-backend-config="key=apps/${{ steps.app.outputs.name }}.tfstate" \
|
||||
-backend-config="endpoint=${{ secrets.SEAWEED_S3_ENDPOINT }}" \
|
||||
-backend-config="region=us-east-1" \
|
||||
-backend-config="force_path_style=true"
|
||||
|
||||
- name: Select workspace
|
||||
run: tofu workspace select "${{ steps.app.outputs.name }}"
|
||||
|
||||
- name: tofu destroy
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.SEAWEED_ACCESS_KEY }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SEAWEED_SECRET_KEY }}
|
||||
run: |
|
||||
tofu destroy -auto-approve \
|
||||
-var-file="${{ matrix.tfvars }}" \
|
||||
-var="ssh_key_path=/tmp/deploy_key" \
|
||||
-var="db_password=${{ steps.secrets.outputs.db_password }}" \
|
||||
-var="openresty_git_token=${{ steps.secrets.outputs.git_token }}"
|
||||
|
||||
- name: Delete workspace
|
||||
run: |
|
||||
tofu workspace select default
|
||||
tofu workspace delete "${{ steps.app.outputs.name }}"
|
||||
|
||||
- name: Cleanup SSH key
|
||||
if: always()
|
||||
run: rm -f /tmp/deploy_key
|
||||
172
.gitea/workflows/test.yml
Normal file
172
.gitea/workflows/test.yml
Normal file
@@ -0,0 +1,172 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- "feature/**"
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
TOFU_VERSION: "1.9.0"
|
||||
|
||||
jobs:
|
||||
# ─── Level 1: Static Analysis ─────────────────────────────────────────────
|
||||
static:
|
||||
name: Static Analysis
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install OpenTofu
|
||||
run: |
|
||||
curl -fsSL https://get.opentofu.org/install-opentofu.sh \
|
||||
| sh -s -- --install-method=standalone --opentofu-version=${{ env.TOFU_VERSION }}
|
||||
|
||||
- name: tofu fmt check (all .tf files)
|
||||
run: tofu fmt -check -recursive
|
||||
|
||||
- name: tofu validate — Docker stack
|
||||
run: |
|
||||
tofu init -backend=false -input=false
|
||||
tofu validate
|
||||
|
||||
- name: tofu validate — K8s stack
|
||||
run: |
|
||||
cd k8s
|
||||
tofu init -backend=false -input=false
|
||||
tofu validate
|
||||
|
||||
# ─── Level 2: Unit Tests (mocked providers) ───────────────────────────────
|
||||
unit-docker:
|
||||
name: Unit Tests — Docker Stack
|
||||
runs-on: ubuntu-latest
|
||||
needs: static
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install OpenTofu
|
||||
run: |
|
||||
curl -fsSL https://get.opentofu.org/install-opentofu.sh \
|
||||
| sh -s -- --install-method=standalone --opentofu-version=${{ env.TOFU_VERSION }}
|
||||
|
||||
- name: tofu test — Docker stack
|
||||
run: |
|
||||
tofu init -backend=false -input=false
|
||||
tofu test
|
||||
|
||||
unit-k8s:
|
||||
name: Unit Tests — K8s Stack
|
||||
runs-on: ubuntu-latest
|
||||
needs: static
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install OpenTofu
|
||||
run: |
|
||||
curl -fsSL https://get.opentofu.org/install-opentofu.sh \
|
||||
| sh -s -- --install-method=standalone --opentofu-version=${{ env.TOFU_VERSION }}
|
||||
|
||||
- name: tofu test — K8s stack
|
||||
run: |
|
||||
cd k8s
|
||||
tofu init -backend=false -input=false
|
||||
tofu test
|
||||
|
||||
# ─── Level 3: Integration — K8s (k3d) ────────────────────────────────────
|
||||
# Runs on push to main only. Creates a real k3d cluster, applies, verifies,
|
||||
# then destroys. Skipped on PRs to keep feedback fast.
|
||||
integration-k8s:
|
||||
name: Integration Test — K8s (k3d)
|
||||
runs-on: ubuntu-latest
|
||||
needs: [unit-docker, unit-k8s]
|
||||
if: github.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install OpenTofu
|
||||
run: |
|
||||
curl -fsSL https://get.opentofu.org/install-opentofu.sh \
|
||||
| sh -s -- --install-method=standalone --opentofu-version=${{ env.TOFU_VERSION }}
|
||||
|
||||
- name: Install k3d
|
||||
run: |
|
||||
curl -s https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash
|
||||
|
||||
- name: Install kubectl
|
||||
run: |
|
||||
curl -fsSL "https://dl.k8s.io/release/$(curl -sL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" \
|
||||
-o /usr/local/bin/kubectl
|
||||
chmod +x /usr/local/bin/kubectl
|
||||
|
||||
- name: Create k3d cluster (Traefik disabled — we install CRDs manually)
|
||||
run: |
|
||||
k3d cluster create tofu-test \
|
||||
--agents 1 \
|
||||
--k3s-arg "--disable=traefik@server:0" \
|
||||
--wait
|
||||
k3d kubeconfig get tofu-test > /tmp/test-kubeconfig
|
||||
chmod 600 /tmp/test-kubeconfig
|
||||
|
||||
- name: Install Traefik CRDs
|
||||
run: |
|
||||
kubectl --kubeconfig /tmp/test-kubeconfig apply \
|
||||
-f https://raw.githubusercontent.com/traefik/traefik/v2.11/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml
|
||||
kubectl --kubeconfig /tmp/test-kubeconfig wait \
|
||||
--for=condition=established --timeout=60s \
|
||||
crd/ingressroutes.traefik.io crd/middlewares.traefik.io
|
||||
|
||||
- name: tofu init — K8s stack
|
||||
working-directory: k8s
|
||||
run: tofu init -backend=false -input=false
|
||||
|
||||
- name: Select or create workspace
|
||||
working-directory: k8s
|
||||
run: |
|
||||
tofu workspace select integration-test \
|
||||
|| tofu workspace new integration-test
|
||||
|
||||
- name: tofu apply
|
||||
working-directory: k8s
|
||||
run: |
|
||||
tofu apply -auto-approve \
|
||||
-var-file="apps/example-nodered.tfvars" \
|
||||
-var="kubeconfig_path=/tmp/test-kubeconfig" \
|
||||
-var="app_name=integration-test" \
|
||||
-var="rabbitmq_password=testpass-ci" \
|
||||
-var="loki_auth_token="
|
||||
|
||||
- name: Verify K8s resources exist
|
||||
run: |
|
||||
NS="integration-test"
|
||||
KC=/tmp/test-kubeconfig
|
||||
echo "── Namespace ──"
|
||||
kubectl --kubeconfig $KC get namespace $NS
|
||||
echo "── Deployment ──"
|
||||
kubectl --kubeconfig $KC get deployment -n $NS
|
||||
echo "── Services ──"
|
||||
kubectl --kubeconfig $KC get service -n $NS
|
||||
echo "── PVCs ──"
|
||||
kubectl --kubeconfig $KC get pvc -n $NS
|
||||
echo "── IngressRoutes ──"
|
||||
kubectl --kubeconfig $KC get ingressroute.traefik.io -n $NS 2>/dev/null || \
|
||||
kubectl --kubeconfig $KC get ingressroute.traefik.containo.us -n $NS
|
||||
|
||||
- name: tofu destroy
|
||||
working-directory: k8s
|
||||
if: always()
|
||||
run: |
|
||||
tofu destroy -auto-approve \
|
||||
-var-file="apps/example-nodered.tfvars" \
|
||||
-var="kubeconfig_path=/tmp/test-kubeconfig" \
|
||||
-var="app_name=integration-test" \
|
||||
-var="rabbitmq_password=testpass-ci" \
|
||||
-var="loki_auth_token="
|
||||
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
run: |
|
||||
k3d cluster delete tofu-test 2>/dev/null || true
|
||||
rm -f /tmp/test-kubeconfig
|
||||
39
.gitlab-ci.yml
Normal file
39
.gitlab-ci.yml
Normal file
@@ -0,0 +1,39 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# GitLab CI/CD — OpenTofu Playground
|
||||
#
|
||||
# Three included pipelines:
|
||||
# test.gitlab-ci.yml Static analysis + unit tests + K8s integration
|
||||
# deploy.gitlab-ci.yml Docker stack deploy/destroy via apps/*.tfvars
|
||||
# deploy-k8s.gitlab-ci.yml K8s stack deploy/destroy via k8s/apps/*.tfvars
|
||||
#
|
||||
# Required GitLab CI/CD variables (Settings > CI/CD > Variables):
|
||||
# TOFU_VERSION OpenTofu version to install (default: 1.9.0)
|
||||
# GOPASS_GPG_KEY GPG private key (armored) for the gopass store
|
||||
# GOPASS_STORE_REPO Git URL of the gopass password store
|
||||
# SEAWEED_S3_ENDPOINT SeaweedFS S3 endpoint, e.g. http://seaweedfs.home:8333
|
||||
# SEAWEED_ACCESS_KEY SeaweedFS access key (mark as Masked)
|
||||
# SEAWEED_SECRET_KEY SeaweedFS secret key (mark as Masked)
|
||||
#
|
||||
# gopass store layout expected by the deploy pipelines:
|
||||
# infra/ssh-keys/<app-name> — SSH private key for Docker host
|
||||
# infra/kubeconfigs/<app-name> — kubeconfig for K8s cluster
|
||||
# apps/<app-name>/db_password — PostgreSQL password
|
||||
# apps/<app-name>/git_token — optional git token for git_clone mode
|
||||
# apps/<app-name>/rabbitmq_password — RabbitMQ password (K8s stack)
|
||||
# apps/<app-name>/loki_token — optional Loki auth token (K8s stack)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
stages:
|
||||
- static
|
||||
- unit
|
||||
- integration
|
||||
- deploy
|
||||
- destroy
|
||||
|
||||
variables:
|
||||
TOFU_VERSION: "1.9.0"
|
||||
|
||||
include:
|
||||
- local: .gitlab/workflows/test.gitlab-ci.yml
|
||||
- local: .gitlab/workflows/deploy.gitlab-ci.yml
|
||||
- local: .gitlab/workflows/deploy-k8s.gitlab-ci.yml
|
||||
198
.gitlab/workflows/deploy-k8s.gitlab-ci.yml
Normal file
198
.gitlab/workflows/deploy-k8s.gitlab-ci.yml
Normal file
@@ -0,0 +1,198 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# K8s stack deploy pipeline — mirrors .gitea/workflows/deploy-k8s.yml
|
||||
#
|
||||
# Triggered on pushes to main when files under k8s/apps/*.tfvars change.
|
||||
# Two jobs run in sequence:
|
||||
# deploy-k8s — loops over added/modified tfvars and applies each app
|
||||
# destroy-k8s — loops over deleted tfvars, recovers content from git
|
||||
# history, destroys each app and removes its workspace
|
||||
#
|
||||
# gopass paths used:
|
||||
# infra/kubeconfigs/<app-name> — kubeconfig for the target cluster
|
||||
# apps/<app-name>/rabbitmq_password — RabbitMQ admin password
|
||||
# apps/<app-name>/loki_token — optional Loki auth bearer token
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
# ─── Shared template: OpenTofu + gopass setup ────────────────────────────────
|
||||
.deploy_k8s_base:
|
||||
image: ubuntu:22.04
|
||||
variables:
|
||||
GIT_DEPTH: "0"
|
||||
TOFU_WORKING_DIR: "k8s"
|
||||
before_script:
|
||||
- apt-get update -qq && apt-get install -y -qq curl git jq gnupg
|
||||
- |
|
||||
curl -fsSL https://get.opentofu.org/install-opentofu.sh \
|
||||
| sh -s -- --install-method=standalone --opentofu-version=$TOFU_VERSION
|
||||
- |
|
||||
curl -fsSL https://github.com/gopasspw/gopass/releases/latest/download/gopass-linux-amd64.tar.gz \
|
||||
| tar xz
|
||||
mv gopass /usr/local/bin/
|
||||
- |
|
||||
echo "$GOPASS_GPG_KEY" | gpg --batch --import
|
||||
gopass clone "$GOPASS_STORE_REPO"
|
||||
|
||||
# ─── Deploy / Update ─────────────────────────────────────────────────────────
|
||||
deploy-k8s:
|
||||
extends: .deploy_k8s_base
|
||||
stage: deploy
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE == "push"'
|
||||
changes:
|
||||
- k8s/apps/*.tfvars
|
||||
script:
|
||||
- |
|
||||
ADDED_MODIFIED=$(git diff --name-only --diff-filter=ACM \
|
||||
"$CI_COMMIT_BEFORE_SHA" "$CI_COMMIT_SHA" -- 'k8s/apps/*.tfvars')
|
||||
|
||||
if [ -z "$ADDED_MODIFIED" ]; then
|
||||
echo "No K8s tfvars files added or modified. Nothing to deploy."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Files to deploy: $ADDED_MODIFIED"
|
||||
FAILED=0
|
||||
|
||||
for TFVARS in $ADDED_MODIFIED; do
|
||||
APP=$(basename "$TFVARS" .tfvars)
|
||||
echo ""
|
||||
echo "══════════════════════════════════════════"
|
||||
echo " Deploying K8s app: $APP"
|
||||
echo "══════════════════════════════════════════"
|
||||
|
||||
if ! (
|
||||
set -e
|
||||
|
||||
# ── Fetch kubeconfig from gopass ──
|
||||
gopass show -o "infra/kubeconfigs/$APP" > /tmp/kubeconfig
|
||||
chmod 600 /tmp/kubeconfig
|
||||
|
||||
# ── Fetch app secrets from gopass ──
|
||||
RABBITMQ_PASSWORD=$(gopass show -o "apps/$APP/rabbitmq_password" 2>/dev/null || echo "")
|
||||
LOKI_AUTH_TOKEN=$(gopass show -o "apps/$APP/loki_token" 2>/dev/null || echo "")
|
||||
|
||||
# ── tofu init with SeaweedFS backend ──
|
||||
export AWS_ACCESS_KEY_ID="$SEAWEED_ACCESS_KEY"
|
||||
export AWS_SECRET_ACCESS_KEY="$SEAWEED_SECRET_KEY"
|
||||
|
||||
cd "$TOFU_WORKING_DIR"
|
||||
tofu init \
|
||||
-backend-config="bucket=tofu-state" \
|
||||
-backend-config="key=apps-k8s/$APP.tfstate" \
|
||||
-backend-config="endpoint=$SEAWEED_S3_ENDPOINT" \
|
||||
-backend-config="region=us-east-1" \
|
||||
-backend-config="force_path_style=true" \
|
||||
-reconfigure
|
||||
|
||||
# ── Workspace ──
|
||||
tofu workspace select "$APP" 2>/dev/null || tofu workspace new "$APP"
|
||||
|
||||
# ── Apply ──
|
||||
tofu apply -auto-approve \
|
||||
-var-file="../$TFVARS" \
|
||||
-var="kubeconfig_path=/tmp/kubeconfig" \
|
||||
-var="rabbitmq_password=$RABBITMQ_PASSWORD" \
|
||||
-var="loki_auth_token=$LOKI_AUTH_TOKEN"
|
||||
|
||||
rm -f /tmp/kubeconfig
|
||||
echo " Deployed K8s app: $APP"
|
||||
); then
|
||||
echo "ERROR: Deployment of K8s app $APP failed — continuing with remaining apps"
|
||||
FAILED=1
|
||||
rm -f /tmp/kubeconfig
|
||||
fi
|
||||
done
|
||||
|
||||
if [ $FAILED -ne 0 ]; then
|
||||
echo ""
|
||||
echo "One or more K8s deployments failed. See logs above for details."
|
||||
exit 1
|
||||
fi
|
||||
after_script:
|
||||
- rm -f /tmp/kubeconfig
|
||||
|
||||
# ─── Destroy (tfvars file deleted) ───────────────────────────────────────────
|
||||
destroy-k8s:
|
||||
extends: .deploy_k8s_base
|
||||
stage: destroy
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE == "push"'
|
||||
changes:
|
||||
- k8s/apps/*.tfvars
|
||||
script:
|
||||
- |
|
||||
DELETED=$(git diff --name-only --diff-filter=D \
|
||||
"$CI_COMMIT_BEFORE_SHA" "$CI_COMMIT_SHA" -- 'k8s/apps/*.tfvars')
|
||||
|
||||
if [ -z "$DELETED" ]; then
|
||||
echo "No K8s tfvars files deleted. Nothing to destroy."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Files to destroy: $DELETED"
|
||||
FAILED=0
|
||||
|
||||
for TFVARS in $DELETED; do
|
||||
APP=$(basename "$TFVARS" .tfvars)
|
||||
echo ""
|
||||
echo "══════════════════════════════════════════"
|
||||
echo " Destroying K8s app: $APP"
|
||||
echo "══════════════════════════════════════════"
|
||||
|
||||
if ! (
|
||||
set -e
|
||||
|
||||
# ── Recover deleted tfvars from git history ──
|
||||
git show "$CI_COMMIT_BEFORE_SHA:$TFVARS" > /tmp/${APP}.tfvars
|
||||
|
||||
# ── Fetch kubeconfig from gopass ──
|
||||
gopass show -o "infra/kubeconfigs/$APP" > /tmp/kubeconfig
|
||||
chmod 600 /tmp/kubeconfig
|
||||
|
||||
# ── Fetch app secrets from gopass ──
|
||||
RABBITMQ_PASSWORD=$(gopass show -o "apps/$APP/rabbitmq_password" 2>/dev/null || echo "")
|
||||
LOKI_AUTH_TOKEN=$(gopass show -o "apps/$APP/loki_token" 2>/dev/null || echo "")
|
||||
|
||||
# ── tofu init ──
|
||||
export AWS_ACCESS_KEY_ID="$SEAWEED_ACCESS_KEY"
|
||||
export AWS_SECRET_ACCESS_KEY="$SEAWEED_SECRET_KEY"
|
||||
|
||||
cd "$TOFU_WORKING_DIR"
|
||||
tofu init \
|
||||
-backend-config="bucket=tofu-state" \
|
||||
-backend-config="key=apps-k8s/$APP.tfstate" \
|
||||
-backend-config="endpoint=$SEAWEED_S3_ENDPOINT" \
|
||||
-backend-config="region=us-east-1" \
|
||||
-backend-config="force_path_style=true" \
|
||||
-reconfigure
|
||||
|
||||
# ── Select existing workspace ──
|
||||
tofu workspace select "$APP"
|
||||
|
||||
# ── Destroy ──
|
||||
tofu destroy -auto-approve \
|
||||
-var-file="/tmp/${APP}.tfvars" \
|
||||
-var="kubeconfig_path=/tmp/kubeconfig" \
|
||||
-var="rabbitmq_password=$RABBITMQ_PASSWORD" \
|
||||
-var="loki_auth_token=$LOKI_AUTH_TOKEN"
|
||||
|
||||
# ── Remove workspace ──
|
||||
tofu workspace select default
|
||||
tofu workspace delete "$APP"
|
||||
|
||||
rm -f /tmp/${APP}.tfvars /tmp/kubeconfig
|
||||
echo " Destroyed K8s app: $APP"
|
||||
); then
|
||||
echo "ERROR: Destroy of K8s app $APP failed — continuing with remaining apps"
|
||||
FAILED=1
|
||||
rm -f /tmp/${APP}.tfvars /tmp/kubeconfig
|
||||
fi
|
||||
done
|
||||
|
||||
if [ $FAILED -ne 0 ]; then
|
||||
echo ""
|
||||
echo "One or more K8s destroy operations failed. See logs above for details."
|
||||
exit 1
|
||||
fi
|
||||
after_script:
|
||||
- rm -f /tmp/kubeconfig /tmp/*.tfvars
|
||||
202
.gitlab/workflows/deploy.gitlab-ci.yml
Normal file
202
.gitlab/workflows/deploy.gitlab-ci.yml
Normal file
@@ -0,0 +1,202 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Docker stack deploy pipeline — mirrors .gitea/workflows/deploy.yml
|
||||
#
|
||||
# Triggered on pushes to main when files under apps/*.tfvars change.
|
||||
# Two jobs run in sequence:
|
||||
# deploy-docker — loops over added/modified tfvars and applies each app
|
||||
# destroy-docker — loops over deleted tfvars, recovers content from git
|
||||
# history, destroys each app and removes its workspace
|
||||
#
|
||||
# Both jobs process all changed files sequentially (GitLab CI does not support
|
||||
# dynamic matrix jobs natively). A failure in one app is recorded but processing
|
||||
# continues for the remaining apps (fail-fast: false equivalent).
|
||||
#
|
||||
# Key difference from Gitea: deleted tfvars content is recovered via
|
||||
# git show $CI_COMMIT_BEFORE_SHA:$TFVARS
|
||||
# instead of checking out the previous commit.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
# ─── Shared template: OpenTofu + gopass setup ────────────────────────────────
|
||||
.deploy_base:
|
||||
image: ubuntu:22.04
|
||||
variables:
|
||||
# Full clone so git show $CI_COMMIT_BEFORE_SHA works for deleted files
|
||||
GIT_DEPTH: "0"
|
||||
before_script:
|
||||
- apt-get update -qq && apt-get install -y -qq curl git jq gnupg
|
||||
- |
|
||||
curl -fsSL https://get.opentofu.org/install-opentofu.sh \
|
||||
| sh -s -- --install-method=standalone --opentofu-version=$TOFU_VERSION
|
||||
- |
|
||||
curl -fsSL https://github.com/gopasspw/gopass/releases/latest/download/gopass-linux-amd64.tar.gz \
|
||||
| tar xz
|
||||
mv gopass /usr/local/bin/
|
||||
- |
|
||||
echo "$GOPASS_GPG_KEY" | gpg --batch --import
|
||||
gopass clone "$GOPASS_STORE_REPO"
|
||||
|
||||
# ─── Deploy / Update ─────────────────────────────────────────────────────────
|
||||
deploy-docker:
|
||||
extends: .deploy_base
|
||||
stage: deploy
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE == "push"'
|
||||
changes:
|
||||
- apps/*.tfvars
|
||||
script:
|
||||
- |
|
||||
ADDED_MODIFIED=$(git diff --name-only --diff-filter=ACM \
|
||||
"$CI_COMMIT_BEFORE_SHA" "$CI_COMMIT_SHA" -- 'apps/*.tfvars')
|
||||
|
||||
if [ -z "$ADDED_MODIFIED" ]; then
|
||||
echo "No Docker tfvars files added or modified. Nothing to deploy."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Files to deploy: $ADDED_MODIFIED"
|
||||
FAILED=0
|
||||
|
||||
for TFVARS in $ADDED_MODIFIED; do
|
||||
APP=$(basename "$TFVARS" .tfvars)
|
||||
echo ""
|
||||
echo "══════════════════════════════════════════"
|
||||
echo " Deploying: $APP"
|
||||
echo "══════════════════════════════════════════"
|
||||
|
||||
if ! (
|
||||
set -e
|
||||
|
||||
# ── Fetch SSH key from gopass ──
|
||||
gopass show -o "infra/ssh-keys/$APP" > /tmp/deploy_key
|
||||
chmod 600 /tmp/deploy_key
|
||||
|
||||
# ── Fetch app secrets from gopass ──
|
||||
DB_PASSWORD=$(gopass show -o "apps/$APP/db_password")
|
||||
GIT_TOKEN=$(gopass show -o "apps/$APP/git_token" 2>/dev/null || echo "")
|
||||
|
||||
# ── tofu init with SeaweedFS backend ──
|
||||
export AWS_ACCESS_KEY_ID="$SEAWEED_ACCESS_KEY"
|
||||
export AWS_SECRET_ACCESS_KEY="$SEAWEED_SECRET_KEY"
|
||||
|
||||
tofu init \
|
||||
-backend-config="bucket=tofu-state" \
|
||||
-backend-config="key=apps/$APP.tfstate" \
|
||||
-backend-config="endpoint=$SEAWEED_S3_ENDPOINT" \
|
||||
-backend-config="region=us-east-1" \
|
||||
-backend-config="force_path_style=true" \
|
||||
-reconfigure
|
||||
|
||||
# ── Workspace ──
|
||||
tofu workspace select "$APP" 2>/dev/null || tofu workspace new "$APP"
|
||||
|
||||
# ── Apply ──
|
||||
tofu apply -auto-approve \
|
||||
-var-file="$TFVARS" \
|
||||
-var="ssh_key_path=/tmp/deploy_key" \
|
||||
-var="db_password=$DB_PASSWORD" \
|
||||
-var="openresty_git_token=$GIT_TOKEN"
|
||||
|
||||
rm -f /tmp/deploy_key
|
||||
echo " Deployed: $APP"
|
||||
); then
|
||||
echo "ERROR: Deployment of $APP failed — continuing with remaining apps"
|
||||
FAILED=1
|
||||
rm -f /tmp/deploy_key
|
||||
fi
|
||||
done
|
||||
|
||||
if [ $FAILED -ne 0 ]; then
|
||||
echo ""
|
||||
echo "One or more deployments failed. See logs above for details."
|
||||
exit 1
|
||||
fi
|
||||
after_script:
|
||||
# Safety cleanup in case the job was interrupted
|
||||
- rm -f /tmp/deploy_key
|
||||
|
||||
# ─── Destroy (tfvars file deleted) ───────────────────────────────────────────
|
||||
destroy-docker:
|
||||
extends: .deploy_base
|
||||
stage: destroy
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE == "push"'
|
||||
changes:
|
||||
- apps/*.tfvars
|
||||
script:
|
||||
- |
|
||||
DELETED=$(git diff --name-only --diff-filter=D \
|
||||
"$CI_COMMIT_BEFORE_SHA" "$CI_COMMIT_SHA" -- 'apps/*.tfvars')
|
||||
|
||||
if [ -z "$DELETED" ]; then
|
||||
echo "No Docker tfvars files deleted. Nothing to destroy."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Files to destroy: $DELETED"
|
||||
FAILED=0
|
||||
|
||||
for TFVARS in $DELETED; do
|
||||
APP=$(basename "$TFVARS" .tfvars)
|
||||
echo ""
|
||||
echo "══════════════════════════════════════════"
|
||||
echo " Destroying: $APP"
|
||||
echo "══════════════════════════════════════════"
|
||||
|
||||
if ! (
|
||||
set -e
|
||||
|
||||
# ── Recover deleted tfvars from git history ──
|
||||
# No need to check out the previous commit — git show reads it directly.
|
||||
git show "$CI_COMMIT_BEFORE_SHA:$TFVARS" > /tmp/${APP}.tfvars
|
||||
|
||||
# ── Fetch SSH key from gopass ──
|
||||
gopass show -o "infra/ssh-keys/$APP" > /tmp/deploy_key
|
||||
chmod 600 /tmp/deploy_key
|
||||
|
||||
# ── Fetch app secrets from gopass ──
|
||||
DB_PASSWORD=$(gopass show -o "apps/$APP/db_password")
|
||||
GIT_TOKEN=$(gopass show -o "apps/$APP/git_token" 2>/dev/null || echo "")
|
||||
|
||||
# ── tofu init ──
|
||||
export AWS_ACCESS_KEY_ID="$SEAWEED_ACCESS_KEY"
|
||||
export AWS_SECRET_ACCESS_KEY="$SEAWEED_SECRET_KEY"
|
||||
|
||||
tofu init \
|
||||
-backend-config="bucket=tofu-state" \
|
||||
-backend-config="key=apps/$APP.tfstate" \
|
||||
-backend-config="endpoint=$SEAWEED_S3_ENDPOINT" \
|
||||
-backend-config="region=us-east-1" \
|
||||
-backend-config="force_path_style=true" \
|
||||
-reconfigure
|
||||
|
||||
# ── Select existing workspace ──
|
||||
tofu workspace select "$APP"
|
||||
|
||||
# ── Destroy ──
|
||||
tofu destroy -auto-approve \
|
||||
-var-file="/tmp/${APP}.tfvars" \
|
||||
-var="ssh_key_path=/tmp/deploy_key" \
|
||||
-var="db_password=$DB_PASSWORD" \
|
||||
-var="openresty_git_token=$GIT_TOKEN"
|
||||
|
||||
# ── Remove workspace ──
|
||||
tofu workspace select default
|
||||
tofu workspace delete "$APP"
|
||||
|
||||
rm -f /tmp/${APP}.tfvars /tmp/deploy_key
|
||||
echo " Destroyed: $APP"
|
||||
); then
|
||||
echo "ERROR: Destroy of $APP failed — continuing with remaining apps"
|
||||
FAILED=1
|
||||
rm -f /tmp/${APP}.tfvars /tmp/deploy_key
|
||||
fi
|
||||
done
|
||||
|
||||
if [ $FAILED -ne 0 ]; then
|
||||
echo ""
|
||||
echo "One or more destroy operations failed. See logs above for details."
|
||||
exit 1
|
||||
fi
|
||||
after_script:
|
||||
# Safety cleanup in case the job was interrupted
|
||||
- rm -f /tmp/deploy_key /tmp/*.tfvars
|
||||
144
.gitlab/workflows/test.gitlab-ci.yml
Normal file
144
.gitlab/workflows/test.gitlab-ci.yml
Normal file
@@ -0,0 +1,144 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Test pipeline — mirrors .gitea/workflows/test.yml
|
||||
#
|
||||
# Level 1 (static) — fmt check + validate, all MRs and feature branches
|
||||
# Level 2 (unit) — tofu test with mock_provider, all MRs and branches
|
||||
# Level 3 (integration) — real k3d cluster apply/destroy, main branch only
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
# ─── Shared template: installs OpenTofu and common tools ─────────────────────
|
||||
.tofu_setup:
|
||||
image: ubuntu:22.04
|
||||
before_script:
|
||||
- apt-get update -qq && apt-get install -y -qq curl git jq
|
||||
- |
|
||||
curl -fsSL https://get.opentofu.org/install-opentofu.sh \
|
||||
| sh -s -- --install-method=standalone --opentofu-version=$TOFU_VERSION
|
||||
|
||||
# ─── Level 1: Static Analysis ────────────────────────────────────────────────
|
||||
static-analysis:
|
||||
extends: .tofu_setup
|
||||
stage: static
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
- if: '$CI_COMMIT_BRANCH == "main"'
|
||||
- if: '$CI_COMMIT_BRANCH =~ /^feature\//'
|
||||
script:
|
||||
- tofu fmt -check -recursive
|
||||
- |
|
||||
echo "── Validating Docker stack ──"
|
||||
tofu init -backend=false -input=false
|
||||
tofu validate
|
||||
- |
|
||||
echo "── Validating K8s stack ──"
|
||||
cd k8s
|
||||
tofu init -backend=false -input=false
|
||||
tofu validate
|
||||
|
||||
# ─── Level 2: Unit Tests (mocked providers) ──────────────────────────────────
|
||||
unit-tests-docker:
|
||||
extends: .tofu_setup
|
||||
stage: unit
|
||||
needs: [static-analysis]
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
- if: '$CI_COMMIT_BRANCH == "main"'
|
||||
- if: '$CI_COMMIT_BRANCH =~ /^feature\//'
|
||||
script:
|
||||
- echo "── Docker stack unit tests ──"
|
||||
- tofu init -backend=false -input=false
|
||||
- tofu test
|
||||
|
||||
unit-tests-k8s:
|
||||
extends: .tofu_setup
|
||||
stage: unit
|
||||
needs: [static-analysis]
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
- if: '$CI_COMMIT_BRANCH == "main"'
|
||||
- if: '$CI_COMMIT_BRANCH =~ /^feature\//'
|
||||
script:
|
||||
- echo "── K8s stack unit tests ──"
|
||||
- cd k8s
|
||||
- tofu init -backend=false -input=false
|
||||
- tofu test
|
||||
|
||||
# ─── Level 3: Integration Test — K8s (k3d) ───────────────────────────────────
|
||||
# Runs only on push to main. Creates a real k3d cluster, applies the example
|
||||
# app, verifies resources exist, then destroys. Skipped on MRs for fast feedback.
|
||||
integration-k8s:
|
||||
extends: .tofu_setup
|
||||
stage: integration
|
||||
needs: [unit-tests-docker, unit-tests-k8s]
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE == "push"'
|
||||
# Full clone needed so git show works for the integration workspace
|
||||
variables:
|
||||
GIT_DEPTH: "0"
|
||||
script:
|
||||
- |
|
||||
echo "── Installing k3d ──"
|
||||
curl -s https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash
|
||||
- |
|
||||
echo "── Installing kubectl ──"
|
||||
curl -fsSL \
|
||||
"https://dl.k8s.io/release/$(curl -sL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" \
|
||||
-o /usr/local/bin/kubectl
|
||||
chmod +x /usr/local/bin/kubectl
|
||||
- |
|
||||
echo "── Creating k3d cluster (Traefik disabled — CRDs installed manually) ──"
|
||||
k3d cluster create tofu-test \
|
||||
--agents 1 \
|
||||
--k3s-arg "--disable=traefik@server:0" \
|
||||
--wait
|
||||
k3d kubeconfig get tofu-test > /tmp/test-kubeconfig
|
||||
chmod 600 /tmp/test-kubeconfig
|
||||
- |
|
||||
echo "── Installing Traefik CRDs ──"
|
||||
kubectl --kubeconfig /tmp/test-kubeconfig apply \
|
||||
-f https://raw.githubusercontent.com/traefik/traefik/v2.11/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml
|
||||
kubectl --kubeconfig /tmp/test-kubeconfig wait \
|
||||
--for=condition=established --timeout=60s \
|
||||
crd/ingressroutes.traefik.io crd/middlewares.traefik.io
|
||||
- |
|
||||
echo "── tofu init + workspace ──"
|
||||
cd k8s
|
||||
tofu init -backend=false -input=false
|
||||
tofu workspace select integration-test 2>/dev/null \
|
||||
|| tofu workspace new integration-test
|
||||
- |
|
||||
echo "── tofu apply ──"
|
||||
cd k8s
|
||||
tofu apply -auto-approve \
|
||||
-var-file="apps/example-nodered.tfvars" \
|
||||
-var="kubeconfig_path=/tmp/test-kubeconfig" \
|
||||
-var="app_name=integration-test" \
|
||||
-var="rabbitmq_password=testpass-ci" \
|
||||
-var="loki_auth_token="
|
||||
- |
|
||||
echo "── Verify K8s resources ──"
|
||||
NS="integration-test"
|
||||
KC=/tmp/test-kubeconfig
|
||||
echo "── Namespace ──"
|
||||
kubectl --kubeconfig $KC get namespace $NS
|
||||
echo "── Deployment ──"
|
||||
kubectl --kubeconfig $KC get deployment -n $NS
|
||||
echo "── Services ──"
|
||||
kubectl --kubeconfig $KC get service -n $NS
|
||||
echo "── PVCs ──"
|
||||
kubectl --kubeconfig $KC get pvc -n $NS
|
||||
echo "── IngressRoutes ──"
|
||||
kubectl --kubeconfig $KC get ingressroute.traefik.io -n $NS 2>/dev/null || \
|
||||
kubectl --kubeconfig $KC get ingressroute.traefik.containo.us -n $NS
|
||||
- |
|
||||
echo "── tofu destroy ──"
|
||||
cd k8s
|
||||
tofu destroy -auto-approve \
|
||||
-var-file="apps/example-nodered.tfvars" \
|
||||
-var="kubeconfig_path=/tmp/test-kubeconfig" \
|
||||
-var="app_name=integration-test" \
|
||||
-var="rabbitmq_password=testpass-ci" \
|
||||
-var="loki_auth_token="
|
||||
after_script:
|
||||
- k3d cluster delete tofu-test 2>/dev/null || true
|
||||
- rm -f /tmp/test-kubeconfig
|
||||
23
.terraform.lock.hcl
generated
Normal file
23
.terraform.lock.hcl
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
# This file is maintained automatically by "tofu init".
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.opentofu.org/kreuzwerker/docker" {
|
||||
version = "3.9.0"
|
||||
constraints = "~> 3.9"
|
||||
hashes = [
|
||||
"h1:Eglp2bA01CEx8KV/K9CMZ1deQaWKW5HVhE2n+2jIp20=",
|
||||
"zh:0ead8281830e9b9496651282235d9a139ba1b1b6ff79e395eb8c78658dc446b9",
|
||||
"zh:0f17d37d8d3872df3fb75c68b5272e0c981343f53b506a9675b4405191edd3ef",
|
||||
"zh:11d50b37323874427c6d2a08b737d3c7707c8301fdd236c94485cf2828d0b14b",
|
||||
"zh:32f6f9b847446054e2db3d72886ef2f1d1aa51a6d0dac42340b07dad18e3f28f",
|
||||
"zh:5ea5c67668b5dcbda560dc6104b788a9bfc974d52f02f7886889b77cc0e5d248",
|
||||
"zh:5fb19a0b07edc344cd3ddeeb9cfb3d183089deb7a6a94a7b22a583aa1712596b",
|
||||
"zh:602a7ece444e2a142ec5245abb98e7a1a990a68afae2df63b6c85ec084f0c5d7",
|
||||
"zh:693dce278524ad8a6d6c9dd7a01bcd63bb85189639198f8d0b044ab0e5099401",
|
||||
"zh:72e9911568103576c6a78fa38841cfd45eeb88ad22a2c649eb140a377a5b3c26",
|
||||
"zh:956b62b6857cbb467b50158601f01b1203daa34cbd447dcc7f044c327e878b68",
|
||||
"zh:9d372bac0d4479868b34485fb4966ba7bb525938f818b6a625f4977004ea83f9",
|
||||
"zh:e06658a51427f9f53dbdb06263406fc1bc56d1a4fb5e7eb660d7cdfc22f596bd",
|
||||
"zh:eee38dadf672b946419af25160eae7c03fc2afbb14f39f2f1d2a7404d647e2f7",
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,870 @@
|
||||
|
||||
<a name="v3.9.0"></a>
|
||||
## [v3.9.0](https://github.com/kreuzwerker/terraform-provider-docker/compare/v3.8.0...v3.9.0) (2025-11-09)
|
||||
|
||||
### Chore
|
||||
|
||||
* Add file requested by hashicorp ([#813](https://github.com/kreuzwerker/terraform-provider-docker/issues/813))
|
||||
* Prepare release v3.8.0 ([#806](https://github.com/kreuzwerker/terraform-provider-docker/issues/806))
|
||||
|
||||
### Feat
|
||||
|
||||
* Implement caching of docker provider ([#808](https://github.com/kreuzwerker/terraform-provider-docker/issues/808))
|
||||
|
||||
### Fix
|
||||
|
||||
* test attribute of docker_service healthcheck is not required ([#815](https://github.com/kreuzwerker/terraform-provider-docker/issues/815))
|
||||
* docker_service label can be updated without recreate ([#814](https://github.com/kreuzwerker/terraform-provider-docker/issues/814))
|
||||
|
||||
|
||||
<a name="v3.8.0"></a>
|
||||
## [v3.8.0](https://github.com/kreuzwerker/terraform-provider-docker/compare/v3.7.0...v3.8.0) (2025-10-08)
|
||||
|
||||
### Feat
|
||||
|
||||
* Add build attribute for docker_registry_image ([#805](https://github.com/kreuzwerker/terraform-provider-docker/issues/805))
|
||||
* Add build option for additional contexts ([#798](https://github.com/kreuzwerker/terraform-provider-docker/issues/798))
|
||||
* implement mac_address for networks_advanced ([#794](https://github.com/kreuzwerker/terraform-provider-docker/issues/794))
|
||||
* Implement docker cluster volume ([#793](https://github.com/kreuzwerker/terraform-provider-docker/issues/793))
|
||||
|
||||
|
||||
<a name="v3.7.0"></a>
|
||||
## [v3.7.0](https://github.com/kreuzwerker/terraform-provider-docker/compare/v3.6.2...v3.7.0) (2025-08-19)
|
||||
|
||||
### Chore
|
||||
|
||||
* Prepare release v3.7.0 ([#774](https://github.com/kreuzwerker/terraform-provider-docker/issues/774))
|
||||
|
||||
### Feat
|
||||
|
||||
* Implement memory_reservation and network_mode enhancements ([#773](https://github.com/kreuzwerker/terraform-provider-docker/issues/773))
|
||||
* Implement cache_from and cache_to for docker_image ([#772](https://github.com/kreuzwerker/terraform-provider-docker/issues/772))
|
||||
|
||||
### Fix
|
||||
|
||||
* Correctly get and set nanoCPUs for docker_container ([#771](https://github.com/kreuzwerker/terraform-provider-docker/issues/771))
|
||||
|
||||
|
||||
<a name="v3.6.2"></a>
|
||||
## [v3.6.2](https://github.com/kreuzwerker/terraform-provider-docker/compare/v3.6.1...v3.6.2) (2025-06-13)
|
||||
|
||||
### Chore
|
||||
|
||||
* Prepare release v3.6.2 ([#750](https://github.com/kreuzwerker/terraform-provider-docker/issues/750))
|
||||
|
||||
### Feat
|
||||
|
||||
* Allow digest in image name ([#744](https://github.com/kreuzwerker/terraform-provider-docker/issues/744))
|
||||
|
||||
### Fix
|
||||
|
||||
* Remove wrong buildkit version assignment ([#747](https://github.com/kreuzwerker/terraform-provider-docker/issues/747))
|
||||
* Reading non existant volume should recreate ([#749](https://github.com/kreuzwerker/terraform-provider-docker/issues/749))
|
||||
* Typo in cgroup_parent handling ([#746](https://github.com/kreuzwerker/terraform-provider-docker/issues/746))
|
||||
|
||||
|
||||
<a name="v3.6.1"></a>
|
||||
## [v3.6.1](https://github.com/kreuzwerker/terraform-provider-docker/compare/v3.6.0...v3.6.1) (2025-06-05)
|
||||
|
||||
### Chore
|
||||
|
||||
* Prepare release v3.6.1 ([#743](https://github.com/kreuzwerker/terraform-provider-docker/issues/743))
|
||||
|
||||
### Feat
|
||||
|
||||
* allow to set the cgroup parent for container ([#609](https://github.com/kreuzwerker/terraform-provider-docker/issues/609))
|
||||
|
||||
|
||||
<a name="v3.6.0"></a>
|
||||
## [v3.6.0](https://github.com/kreuzwerker/terraform-provider-docker/compare/v3.5.0...v3.6.0) (2025-05-25)
|
||||
|
||||
### Chore
|
||||
|
||||
* Prepare release v3.6.0 ([#735](https://github.com/kreuzwerker/terraform-provider-docker/issues/735))
|
||||
|
||||
### Feat
|
||||
|
||||
* Implement correct cpu scheduler settings ([#732](https://github.com/kreuzwerker/terraform-provider-docker/issues/732))
|
||||
* Add implementaion of capabilities in docker servic ([#727](https://github.com/kreuzwerker/terraform-provider-docker/issues/727))
|
||||
* implement Buildx builder resource ([#724](https://github.com/kreuzwerker/terraform-provider-docker/issues/724))
|
||||
|
||||
### Fix
|
||||
|
||||
* Implement buildx fixes for general buildkit support and platform handling ([#734](https://github.com/kreuzwerker/terraform-provider-docker/issues/734))
|
||||
* Make endpoint validation less strict ([#733](https://github.com/kreuzwerker/terraform-provider-docker/issues/733))
|
||||
|
||||
|
||||
<a name="v3.5.0"></a>
|
||||
## [v3.5.0](https://github.com/kreuzwerker/terraform-provider-docker/compare/v3.4.0...v3.5.0) (2025-05-06)
|
||||
|
||||
### Chore
|
||||
|
||||
* Prepare release v3.5.0 ([#721](https://github.com/kreuzwerker/terraform-provider-docker/issues/721))
|
||||
|
||||
### Feat
|
||||
|
||||
* Implement using of buildx for docker_image ([#717](https://github.com/kreuzwerker/terraform-provider-docker/issues/717))
|
||||
* Support registries that return empty auth scope [#646](https://github.com/kreuzwerker/terraform-provider-docker/issues/646)
|
||||
* Implement registry_image_manifests data source ([#714](https://github.com/kreuzwerker/terraform-provider-docker/issues/714))
|
||||
* Implement healthcheck start interval ([#713](https://github.com/kreuzwerker/terraform-provider-docker/issues/713))
|
||||
|
||||
|
||||
<a name="v3.4.0"></a>
|
||||
## [v3.4.0](https://github.com/kreuzwerker/terraform-provider-docker/compare/v3.3.0...v3.4.0) (2025-04-25)
|
||||
|
||||
### Chore
|
||||
|
||||
* Prepare release v3.4.0 ([#712](https://github.com/kreuzwerker/terraform-provider-docker/issues/712))
|
||||
|
||||
### Feat
|
||||
|
||||
* Implement volume_options subpath ([#710](https://github.com/kreuzwerker/terraform-provider-docker/issues/710))
|
||||
|
||||
### Fix
|
||||
|
||||
* Prevent recreation of image name is intentionally set to a fixed value ([#711](https://github.com/kreuzwerker/terraform-provider-docker/issues/711))
|
||||
* Improve container wait handling ([#709](https://github.com/kreuzwerker/terraform-provider-docker/issues/709))
|
||||
* Use auth_config block also for registry_image delete functionality ([#708](https://github.com/kreuzwerker/terraform-provider-docker/issues/708))
|
||||
|
||||
|
||||
<a name="v3.3.0"></a>
|
||||
## [v3.3.0](https://github.com/kreuzwerker/terraform-provider-docker/compare/v3.2.0...v3.3.0) (2025-04-19)
|
||||
|
||||
### Chore
|
||||
|
||||
* Prepare release v3.3.0 ([#705](https://github.com/kreuzwerker/terraform-provider-docker/issues/705))
|
||||
* Update terraform-plugin-sdk/v2 dependency ([#699](https://github.com/kreuzwerker/terraform-provider-docker/issues/699))
|
||||
* Update docker/docker and docker/cli to newest stable ([#695](https://github.com/kreuzwerker/terraform-provider-docker/issues/695))
|
||||
|
||||
### Feat
|
||||
|
||||
* Implement support for docker context ([#704](https://github.com/kreuzwerker/terraform-provider-docker/issues/704))
|
||||
* disable_docker_daemon_check for provider ([#703](https://github.com/kreuzwerker/terraform-provider-docker/issues/703))
|
||||
* Implement tag triggers for docker_tag resource ([#702](https://github.com/kreuzwerker/terraform-provider-docker/issues/702))
|
||||
* Implement auth_config for docker_registry_image ([#701](https://github.com/kreuzwerker/terraform-provider-docker/issues/701))
|
||||
|
||||
### Fix
|
||||
|
||||
* Store correctly ports from server ([#698](https://github.com/kreuzwerker/terraform-provider-docker/issues/698))
|
||||
|
||||
|
||||
<a name="v3.2.0"></a>
|
||||
## [v3.2.0](https://github.com/kreuzwerker/terraform-provider-docker/compare/v3.1.2...v3.2.0) (2025-04-16)
|
||||
|
||||
### Chore
|
||||
|
||||
* Prepare release v3.2.0 ([#694](https://github.com/kreuzwerker/terraform-provider-docker/issues/694))
|
||||
* Upgrade golangci-lint to next major version ([#686](https://github.com/kreuzwerker/terraform-provider-docker/issues/686))
|
||||
|
||||
### Docs
|
||||
|
||||
* Consolidated update of docs from several PRs ([#691](https://github.com/kreuzwerker/terraform-provider-docker/issues/691))
|
||||
|
||||
### Feat
|
||||
|
||||
* Implement upload permissions in docker_container resource ([#693](https://github.com/kreuzwerker/terraform-provider-docker/issues/693))
|
||||
* Implement docker_image timeouts ([#692](https://github.com/kreuzwerker/terraform-provider-docker/issues/692))
|
||||
* Add support for build-secrets ([#604](https://github.com/kreuzwerker/terraform-provider-docker/issues/604))
|
||||
|
||||
### Fix
|
||||
|
||||
* Authentication to ECR public ([#690](https://github.com/kreuzwerker/terraform-provider-docker/issues/690))
|
||||
|
||||
|
||||
<a name="v3.1.2"></a>
|
||||
## [v3.1.2](https://github.com/kreuzwerker/terraform-provider-docker/compare/v3.1.1...v3.1.2) (2025-04-15)
|
||||
|
||||
### Chore
|
||||
|
||||
* prepare release 3.1.2 ([#688](https://github.com/kreuzwerker/terraform-provider-docker/issues/688))
|
||||
|
||||
|
||||
<a name="v3.1.1"></a>
|
||||
## [v3.1.1](https://github.com/kreuzwerker/terraform-provider-docker/compare/v3.1.0...v3.1.1) (2025-04-14)
|
||||
|
||||
### Chore
|
||||
|
||||
* Prepare release 3.1.1 ([#687](https://github.com/kreuzwerker/terraform-provider-docker/issues/687))
|
||||
|
||||
|
||||
<a name="v3.1.0"></a>
|
||||
## [v3.1.0](https://github.com/kreuzwerker/terraform-provider-docker/compare/v3.0.2...v3.1.0) (2025-04-14)
|
||||
|
||||
### Chore
|
||||
|
||||
* Prepare release 3.1.0 ([#685](https://github.com/kreuzwerker/terraform-provider-docker/issues/685))
|
||||
* update Go version to 1.22 for consistency across workflows, jo… ([#613](https://github.com/kreuzwerker/terraform-provider-docker/issues/613))
|
||||
|
||||
### Feat
|
||||
|
||||
* support setting cpu shares ([#575](https://github.com/kreuzwerker/terraform-provider-docker/issues/575))
|
||||
|
||||
### Fix
|
||||
|
||||
* Use build_args everywhere and update documentation ([#681](https://github.com/kreuzwerker/terraform-provider-docker/issues/681))
|
||||
* Compress build context before sending it to Docker ([#461](https://github.com/kreuzwerker/terraform-provider-docker/issues/461))
|
||||
* Set correct default network driver and fix a test ([#677](https://github.com/kreuzwerker/terraform-provider-docker/issues/677))
|
||||
|
||||
### Typo
|
||||
|
||||
* s/presend/present/ ([#606](https://github.com/kreuzwerker/terraform-provider-docker/issues/606))
|
||||
|
||||
|
||||
<a name="v3.0.2"></a>
|
||||
## [v3.0.2](https://github.com/kreuzwerker/terraform-provider-docker/compare/v3.0.1...v3.0.2) (2023-03-17)
|
||||
|
||||
### Chore
|
||||
|
||||
* Prepare release v3.0.2
|
||||
|
||||
### Docs
|
||||
|
||||
* correct spelling of "networks_advanced" ([#517](https://github.com/kreuzwerker/terraform-provider-docker/issues/517))
|
||||
|
||||
### Fix
|
||||
|
||||
* Implement proxy support. ([#529](https://github.com/kreuzwerker/terraform-provider-docker/issues/529))
|
||||
|
||||
|
||||
<a name="v3.0.1"></a>
|
||||
## [v3.0.1](https://github.com/kreuzwerker/terraform-provider-docker/compare/v3.0.0...v3.0.1) (2023-01-13)
|
||||
|
||||
### Chore
|
||||
|
||||
* Prepare release v3.0.1
|
||||
|
||||
### Fix
|
||||
|
||||
* Access health of container correctly. ([#506](https://github.com/kreuzwerker/terraform-provider-docker/issues/506))
|
||||
|
||||
|
||||
<a name="v3.0.0"></a>
|
||||
## [v3.0.0](https://github.com/kreuzwerker/terraform-provider-docker/compare/v2.25.0...v3.0.0) (2023-01-13)
|
||||
|
||||
### Chore
|
||||
|
||||
* Prepare release v3.0.0
|
||||
|
||||
### Docs
|
||||
|
||||
* Update documentation.
|
||||
* Add migration guide and update README ([#502](https://github.com/kreuzwerker/terraform-provider-docker/issues/502))
|
||||
|
||||
### Feat
|
||||
|
||||
* Prepare v3 release ([#503](https://github.com/kreuzwerker/terraform-provider-docker/issues/503))
|
||||
|
||||
|
||||
<a name="v2.25.0"></a>
|
||||
## [v2.25.0](https://github.com/kreuzwerker/terraform-provider-docker/compare/v2.24.0...v2.25.0) (2023-01-05)
|
||||
|
||||
### Chore
|
||||
|
||||
* Prepare release v2.25.0
|
||||
|
||||
### Docs
|
||||
|
||||
* Add documentation of remote hosts. ([#498](https://github.com/kreuzwerker/terraform-provider-docker/issues/498))
|
||||
|
||||
### Feat
|
||||
|
||||
* Migrate build block to `docker_image` ([#501](https://github.com/kreuzwerker/terraform-provider-docker/issues/501))
|
||||
* Add platform attribute to docker_image resource ([#500](https://github.com/kreuzwerker/terraform-provider-docker/issues/500))
|
||||
* Add sysctl implementation to container of docker_service. ([#499](https://github.com/kreuzwerker/terraform-provider-docker/issues/499))
|
||||
|
||||
|
||||
<a name="v2.24.0"></a>
|
||||
## [v2.24.0](https://github.com/kreuzwerker/terraform-provider-docker/compare/v2.23.1...v2.24.0) (2022-12-23)
|
||||
|
||||
### Chore
|
||||
|
||||
* Prepare release v2.24.0
|
||||
|
||||
### Docs
|
||||
|
||||
* Fix generated website.
|
||||
* Update command typo ([#487](https://github.com/kreuzwerker/terraform-provider-docker/issues/487))
|
||||
|
||||
### Feat
|
||||
|
||||
* cgroupns support ([#497](https://github.com/kreuzwerker/terraform-provider-docker/issues/497))
|
||||
* Add triggers attribute to docker_registry_image ([#496](https://github.com/kreuzwerker/terraform-provider-docker/issues/496))
|
||||
* Support registries with disabled auth ([#494](https://github.com/kreuzwerker/terraform-provider-docker/issues/494))
|
||||
* add IPAM options block for docker networks ([#491](https://github.com/kreuzwerker/terraform-provider-docker/issues/491))
|
||||
|
||||
### Fix
|
||||
|
||||
* Pin data source specific tag test to older tag.
|
||||
|
||||
### Tests
|
||||
|
||||
* Add test for parsing auth headers.
|
||||
|
||||
|
||||
<a name="v2.23.1"></a>
|
||||
## [v2.23.1](https://github.com/kreuzwerker/terraform-provider-docker/compare/v2.23.0...v2.23.1) (2022-11-23)
|
||||
|
||||
### Chore
|
||||
|
||||
* Prepare release v2.23.1
|
||||
|
||||
### Fix
|
||||
|
||||
* Update shasum of busybox:1.35.0 tag in test.
|
||||
* Handle Auth Header Scopes ([#482](https://github.com/kreuzwerker/terraform-provider-docker/issues/482))
|
||||
* Set OS_ARCH from GOHOSTOS and GOHOSTARCH ([#477](https://github.com/kreuzwerker/terraform-provider-docker/issues/477))
|
||||
|
||||
|
||||
<a name="v2.23.0"></a>
|
||||
## [v2.23.0](https://github.com/kreuzwerker/terraform-provider-docker/compare/v2.22.0...v2.23.0) (2022-11-02)
|
||||
|
||||
### Chore
|
||||
|
||||
* Prepare release v2.23.0
|
||||
|
||||
### Feat
|
||||
|
||||
* wait container healthy state ([#467](https://github.com/kreuzwerker/terraform-provider-docker/issues/467))
|
||||
* add docker logs data source ([#471](https://github.com/kreuzwerker/terraform-provider-docker/issues/471))
|
||||
|
||||
### Fix
|
||||
|
||||
* Update shasum of busybox:1.35.0 tag in test.
|
||||
* Update shasum of busybox:1.35.0 tag
|
||||
* Correct provider name to match the public registry ([#462](https://github.com/kreuzwerker/terraform-provider-docker/issues/462))
|
||||
|
||||
|
||||
<a name="v2.22.0"></a>
|
||||
## [v2.22.0](https://github.com/kreuzwerker/terraform-provider-docker/compare/v2.21.0...v2.22.0) (2022-09-20)
|
||||
|
||||
### Chore
|
||||
|
||||
* Prepare release v2.22.0
|
||||
|
||||
### Feat
|
||||
|
||||
* Configurable timeout for docker_container resource stateChangeConf ([#454](https://github.com/kreuzwerker/terraform-provider-docker/issues/454))
|
||||
|
||||
### Fix
|
||||
|
||||
* oauth authorization support for azurecr ([#451](https://github.com/kreuzwerker/terraform-provider-docker/issues/451))
|
||||
|
||||
|
||||
<a name="v2.21.0"></a>
|
||||
## [v2.21.0](https://github.com/kreuzwerker/terraform-provider-docker/compare/v2.20.3...v2.21.0) (2022-09-05)
|
||||
|
||||
### Chore
|
||||
|
||||
* Prepare release v2.21.0
|
||||
|
||||
### Docs
|
||||
|
||||
* Fix docker config example.
|
||||
|
||||
### Feat
|
||||
|
||||
* Add image_id attribute to docker_image resource. ([#450](https://github.com/kreuzwerker/terraform-provider-docker/issues/450))
|
||||
* Update used goversion to 1.18. ([#449](https://github.com/kreuzwerker/terraform-provider-docker/issues/449))
|
||||
|
||||
### Fix
|
||||
|
||||
* Replace deprecated .latest attribute with new image_id. ([#453](https://github.com/kreuzwerker/terraform-provider-docker/issues/453))
|
||||
* Remove reading part of docker_tag resource. ([#448](https://github.com/kreuzwerker/terraform-provider-docker/issues/448))
|
||||
* Fix repo_digest value for DockerImageDatasource test.
|
||||
|
||||
|
||||
<a name="v2.20.3"></a>
|
||||
## [v2.20.3](https://github.com/kreuzwerker/terraform-provider-docker/compare/v2.20.2...v2.20.3) (2022-08-31)
|
||||
|
||||
### Chore
|
||||
|
||||
* Prepare release v2.20.3
|
||||
|
||||
### Fix
|
||||
|
||||
* Docker Registry Image data source use HEAD request to query image digest ([#433](https://github.com/kreuzwerker/terraform-provider-docker/issues/433))
|
||||
* Adding Support for Windows Paths in Bash ([#438](https://github.com/kreuzwerker/terraform-provider-docker/issues/438))
|
||||
|
||||
|
||||
<a name="v2.20.2"></a>
|
||||
## [v2.20.2](https://github.com/kreuzwerker/terraform-provider-docker/compare/v2.20.1...v2.20.2) (2022-08-10)
|
||||
|
||||
### Chore
|
||||
|
||||
* Prepare release v2.20.2
|
||||
|
||||
### Fix
|
||||
|
||||
* Check the operating system for determining the default Docker socket ([#427](https://github.com/kreuzwerker/terraform-provider-docker/issues/427))
|
||||
|
||||
### Reverts
|
||||
|
||||
* fix(deps): update module github.com/golangci/golangci-lint to v1.48.0 ([#423](https://github.com/kreuzwerker/terraform-provider-docker/issues/423))
|
||||
|
||||
|
||||
<a name="v2.20.1"></a>
|
||||
## [v2.20.1](https://github.com/kreuzwerker/terraform-provider-docker/compare/v2.20.0...v2.20.1) (2022-08-10)
|
||||
|
||||
### Chore
|
||||
|
||||
* Prepare release v2.20.1
|
||||
* Reduce time to setup AccTests ([#430](https://github.com/kreuzwerker/terraform-provider-docker/issues/430))
|
||||
|
||||
### Docs
|
||||
|
||||
* Improve docker network usage documentation [skip-ci]
|
||||
|
||||
### Feat
|
||||
|
||||
* Implement triggers attribute for docker_image. ([#425](https://github.com/kreuzwerker/terraform-provider-docker/issues/425))
|
||||
|
||||
### Fix
|
||||
|
||||
* Add ForceTrue to docker_image name attribute. ([#421](https://github.com/kreuzwerker/terraform-provider-docker/issues/421))
|
||||
|
||||
|
||||
<a name="v2.20.0"></a>
|
||||
## [v2.20.0](https://github.com/kreuzwerker/terraform-provider-docker/compare/v2.19.0...v2.20.0) (2022-07-28)
|
||||
|
||||
### Chore
|
||||
|
||||
* Prepare release v2.20.0
|
||||
* Fix release targets in Makefile.
|
||||
|
||||
### Feat
|
||||
|
||||
* Implementation of `docker_tag` resource. ([#418](https://github.com/kreuzwerker/terraform-provider-docker/issues/418))
|
||||
* Implement support for insecure registries ([#414](https://github.com/kreuzwerker/terraform-provider-docker/issues/414))
|
||||
|
||||
|
||||
<a name="v2.19.0"></a>
|
||||
## [v2.19.0](https://github.com/kreuzwerker/terraform-provider-docker/compare/v2.18.1...v2.19.0) (2022-07-15)
|
||||
|
||||
### Chore
|
||||
|
||||
* Prepare release v2.19.0
|
||||
|
||||
### Feat
|
||||
|
||||
* Add gpu flag to docker_container resource ([#405](https://github.com/kreuzwerker/terraform-provider-docker/issues/405))
|
||||
|
||||
### Fix
|
||||
|
||||
* Enable authentication to multiple registries again. ([#400](https://github.com/kreuzwerker/terraform-provider-docker/issues/400))
|
||||
* ECR authentication ([#409](https://github.com/kreuzwerker/terraform-provider-docker/issues/409))
|
||||
|
||||
|
||||
<a name="v2.18.1"></a>
|
||||
## [v2.18.1](https://github.com/kreuzwerker/terraform-provider-docker/compare/v2.18.0...v2.18.1) (2022-07-14)
|
||||
|
||||
### Chore
|
||||
|
||||
* Prepare release v2.18.1
|
||||
* Automate changelog generation [skip ci]
|
||||
|
||||
### Fix
|
||||
|
||||
* Improve searchLocalImages error handling. ([#407](https://github.com/kreuzwerker/terraform-provider-docker/issues/407))
|
||||
* Throw errors when any part of docker config file handling goes wrong. ([#406](https://github.com/kreuzwerker/terraform-provider-docker/issues/406))
|
||||
* Enables having a Dockerfile outside the context ([#402](https://github.com/kreuzwerker/terraform-provider-docker/issues/402))
|
||||
|
||||
|
||||
<a name="v2.18.0"></a>
|
||||
## [v2.18.0](https://github.com/kreuzwerker/terraform-provider-docker/compare/v2.17.0...v2.18.0) (2022-07-11)
|
||||
|
||||
### Chore
|
||||
|
||||
* prepare release v2.18.0
|
||||
|
||||
### Feat
|
||||
|
||||
* add runtime, stop_signal and stop_timeout properties to the docker_container resource ([#364](https://github.com/kreuzwerker/terraform-provider-docker/issues/364))
|
||||
|
||||
### Fix
|
||||
|
||||
* Correctly handle build files and context for docker_registry_image ([#398](https://github.com/kreuzwerker/terraform-provider-docker/issues/398))
|
||||
* Switch to proper go tools mechanism to fix website-* workflows. ([#399](https://github.com/kreuzwerker/terraform-provider-docker/issues/399))
|
||||
* compare relative paths when excluding, fixes kreuzwerker[#280](https://github.com/kreuzwerker/terraform-provider-docker/issues/280) ([#397](https://github.com/kreuzwerker/terraform-provider-docker/issues/397))
|
||||
|
||||
|
||||
<a name="v2.17.0"></a>
|
||||
## [v2.17.0](https://github.com/kreuzwerker/terraform-provider-docker/compare/v2.16.0...v2.17.0) (2022-06-23)
|
||||
|
||||
### Chore
|
||||
|
||||
* prepare release v2.17.0
|
||||
* Exclude examples directory from renovate.
|
||||
* remove the workflow to close stale issues and pull requests ([#371](https://github.com/kreuzwerker/terraform-provider-docker/issues/371))
|
||||
|
||||
### Fix
|
||||
|
||||
* update go package files directly on master to fix build.
|
||||
* correct authentication for ghcr.io registry([#349](https://github.com/kreuzwerker/terraform-provider-docker/issues/349))
|
||||
|
||||
|
||||
<a name="v2.16.0"></a>
|
||||
## [v2.16.0](https://github.com/kreuzwerker/terraform-provider-docker/compare/v2.15.0...v2.16.0) (2022-01-24)
|
||||
|
||||
### Chore
|
||||
|
||||
* prepare release v2.16.0
|
||||
|
||||
### Docs
|
||||
|
||||
* fix service options ([#337](https://github.com/kreuzwerker/terraform-provider-docker/issues/337))
|
||||
* update registry_image.md ([#321](https://github.com/kreuzwerker/terraform-provider-docker/issues/321))
|
||||
* fix r/registry_image truncated docs ([#304](https://github.com/kreuzwerker/terraform-provider-docker/issues/304))
|
||||
|
||||
### Feat
|
||||
|
||||
* add parameter for SSH options ([#335](https://github.com/kreuzwerker/terraform-provider-docker/issues/335))
|
||||
|
||||
### Fix
|
||||
|
||||
* pass container rm flag ([#322](https://github.com/kreuzwerker/terraform-provider-docker/issues/322))
|
||||
* add nil check of DriverConfig ([#315](https://github.com/kreuzwerker/terraform-provider-docker/issues/315))
|
||||
* fmt of go files for go 1.17
|
||||
|
||||
|
||||
<a name="v2.15.0"></a>
|
||||
## [v2.15.0](https://github.com/kreuzwerker/terraform-provider-docker/compare/v2.14.0...v2.15.0) (2021-08-11)
|
||||
|
||||
### Chore
|
||||
|
||||
* prepare release v2.15.0
|
||||
* re go gets terraform-plugin-docs
|
||||
|
||||
### Docs
|
||||
|
||||
* corrects authentication misspell. Closes [#264](https://github.com/kreuzwerker/terraform-provider-docker/issues/264)
|
||||
|
||||
### Feat
|
||||
|
||||
* add container storage opts ([#258](https://github.com/kreuzwerker/terraform-provider-docker/issues/258))
|
||||
|
||||
### Fix
|
||||
|
||||
* add current timestamp for file upload to container ([#259](https://github.com/kreuzwerker/terraform-provider-docker/issues/259))
|
||||
|
||||
|
||||
<a name="v2.14.0"></a>
|
||||
## [v2.14.0](https://github.com/kreuzwerker/terraform-provider-docker/compare/v2.13.0...v2.14.0) (2021-07-09)
|
||||
|
||||
### Chore
|
||||
|
||||
* prepare release v2.14.0
|
||||
|
||||
### Docs
|
||||
|
||||
* update to absolute path for registry image context ([#246](https://github.com/kreuzwerker/terraform-provider-docker/issues/246))
|
||||
* update readme with logos and subsections ([#235](https://github.com/kreuzwerker/terraform-provider-docker/issues/235))
|
||||
|
||||
### Feat
|
||||
|
||||
* support terraform v1 ([#242](https://github.com/kreuzwerker/terraform-provider-docker/issues/242))
|
||||
|
||||
### Fix
|
||||
|
||||
* Update the URL of the docker hub registry ([#230](https://github.com/kreuzwerker/terraform-provider-docker/issues/230))
|
||||
|
||||
|
||||
<a name="v2.13.0"></a>
|
||||
## [v2.13.0](https://github.com/kreuzwerker/terraform-provider-docker/compare/v2.12.2...v2.13.0) (2021-06-22)
|
||||
|
||||
### Chore
|
||||
|
||||
* prepare release v2.13.0
|
||||
|
||||
### Docs
|
||||
|
||||
* fix a few typos ([#216](https://github.com/kreuzwerker/terraform-provider-docker/issues/216))
|
||||
* fix typos in docker_image example usage ([#213](https://github.com/kreuzwerker/terraform-provider-docker/issues/213))
|
||||
|
||||
|
||||
<a name="v2.12.2"></a>
|
||||
## [v2.12.2](https://github.com/kreuzwerker/terraform-provider-docker/compare/v2.12.1...v2.12.2) (2021-05-26)
|
||||
|
||||
### Chore
|
||||
|
||||
* prepare release v2.12.2
|
||||
|
||||
|
||||
<a name="v2.12.1"></a>
|
||||
## [v2.12.1](https://github.com/kreuzwerker/terraform-provider-docker/compare/v2.12.0...v2.12.1) (2021-05-26)
|
||||
|
||||
### Chore
|
||||
|
||||
* update changelog for v2.12.1
|
||||
|
||||
### Fix
|
||||
|
||||
* add service host flattener with space split ([#205](https://github.com/kreuzwerker/terraform-provider-docker/issues/205))
|
||||
* service state upgradeV2 for empty auth
|
||||
|
||||
|
||||
<a name="v2.12.0"></a>
|
||||
## [v2.12.0](https://github.com/kreuzwerker/terraform-provider-docker/compare/v2.11.0...v2.12.0) (2021-05-23)
|
||||
|
||||
### Chore
|
||||
|
||||
* update changelog for v2.12.0
|
||||
* ignore dist folder
|
||||
* configure actions/stale ([#157](https://github.com/kreuzwerker/terraform-provider-docker/issues/157))
|
||||
* add the guide about Terraform Configuration in Bug Report ([#139](https://github.com/kreuzwerker/terraform-provider-docker/issues/139))
|
||||
* bump docker dependency to v20.10.5 ([#119](https://github.com/kreuzwerker/terraform-provider-docker/issues/119))
|
||||
|
||||
### Ci
|
||||
|
||||
* run acceptance tests with multiple Terraform versions ([#129](https://github.com/kreuzwerker/terraform-provider-docker/issues/129))
|
||||
|
||||
### Docs
|
||||
|
||||
* update for v2.12.0
|
||||
* add releasing steps
|
||||
* format `Guide of Bug report` ([#159](https://github.com/kreuzwerker/terraform-provider-docker/issues/159))
|
||||
* add an example to build an image with docker_image ([#158](https://github.com/kreuzwerker/terraform-provider-docker/issues/158))
|
||||
* add a guide about writing issues to CONTRIBUTING.md ([#149](https://github.com/kreuzwerker/terraform-provider-docker/issues/149))
|
||||
* fix Github repository URL in README ([#136](https://github.com/kreuzwerker/terraform-provider-docker/issues/136))
|
||||
|
||||
### Feat
|
||||
|
||||
* support darwin arm builds and golang 1.16 ([#140](https://github.com/kreuzwerker/terraform-provider-docker/issues/140))
|
||||
* migrate to terraform-sdk v2 ([#102](https://github.com/kreuzwerker/terraform-provider-docker/issues/102))
|
||||
|
||||
### Fix
|
||||
|
||||
* rewriting tar header fields ([#198](https://github.com/kreuzwerker/terraform-provider-docker/issues/198))
|
||||
* test spaces for windows ([#190](https://github.com/kreuzwerker/terraform-provider-docker/issues/190))
|
||||
* replace for loops with StateChangeConf ([#182](https://github.com/kreuzwerker/terraform-provider-docker/issues/182))
|
||||
* skip sign on compile action
|
||||
* assign map to rawState when it is nil to prevent panic ([#180](https://github.com/kreuzwerker/terraform-provider-docker/issues/180))
|
||||
* search local images with Docker image ID ([#151](https://github.com/kreuzwerker/terraform-provider-docker/issues/151))
|
||||
* set "ForceNew: true" to labelSchema ([#152](https://github.com/kreuzwerker/terraform-provider-docker/issues/152))
|
||||
|
||||
|
||||
<a name="v2.11.0"></a>
|
||||
## [v2.11.0](https://github.com/kreuzwerker/terraform-provider-docker/compare/v2.10.0...v2.11.0) (2021-01-22)
|
||||
|
||||
### Chore
|
||||
|
||||
* update changelog for v2.11.0
|
||||
* updates changelog for v2.10.0
|
||||
|
||||
### Docs
|
||||
|
||||
* fix legacy configuration style ([#126](https://github.com/kreuzwerker/terraform-provider-docker/issues/126))
|
||||
|
||||
### Feat
|
||||
|
||||
* add properties -it (tty and stdin_opn) to docker container
|
||||
|
||||
|
||||
<a name="v2.10.0"></a>
|
||||
## [v2.10.0](https://github.com/kreuzwerker/terraform-provider-docker/compare/v2.9.0...v2.10.0) (2021-01-08)
|
||||
|
||||
### Chore
|
||||
|
||||
* updates changelog for 2.10.0
|
||||
* ignores testing folders
|
||||
* adds separate bug and ft req templates
|
||||
|
||||
### Ci
|
||||
|
||||
* bumps to docker version 20.10.1
|
||||
* pins workflows to ubuntu:20.04 image
|
||||
|
||||
### Docs
|
||||
|
||||
* add labels to arguments of docker_service ([#105](https://github.com/kreuzwerker/terraform-provider-docker/issues/105))
|
||||
* cleans readme
|
||||
* adds coc and contributing
|
||||
|
||||
### Feat
|
||||
|
||||
* supports Docker plugin ([#35](https://github.com/kreuzwerker/terraform-provider-docker/issues/35))
|
||||
* support max replicas of Docker Service Task Spec ([#112](https://github.com/kreuzwerker/terraform-provider-docker/issues/112))
|
||||
* add force_remove option to r/image ([#104](https://github.com/kreuzwerker/terraform-provider-docker/issues/104))
|
||||
* add local semantic commit validation ([#99](https://github.com/kreuzwerker/terraform-provider-docker/issues/99))
|
||||
* add ability to lint/check of links in documentation locally ([#98](https://github.com/kreuzwerker/terraform-provider-docker/issues/98))
|
||||
|
||||
### Fix
|
||||
|
||||
* set "latest" to tag when tag isn't specified ([#117](https://github.com/kreuzwerker/terraform-provider-docker/issues/117))
|
||||
* image label for workflows
|
||||
* remove all azure cps
|
||||
|
||||
### Pull Requests
|
||||
|
||||
* Merge pull request [#38](https://github.com/kreuzwerker/terraform-provider-docker/issues/38) from kreuzwerker/ci-ubuntu2004-workflow
|
||||
* Merge pull request [#36](https://github.com/kreuzwerker/terraform-provider-docker/issues/36) from kreuzwerker/chore-gh-issue-tpl
|
||||
|
||||
|
||||
<a name="v2.9.0"></a>
|
||||
## [v2.9.0](https://github.com/kreuzwerker/terraform-provider-docker/compare/v2.8.0...v2.9.0) (2020-12-25)
|
||||
|
||||
### Chore
|
||||
|
||||
* updates changelog for 2.9.0
|
||||
* update changelog 2.8.0 release date
|
||||
* introduces golangci-lint ([#32](https://github.com/kreuzwerker/terraform-provider-docker/issues/32))
|
||||
* fix changelog links
|
||||
|
||||
### Ci
|
||||
|
||||
* add gofmt's '-s' option
|
||||
* remove unneeded make tasks
|
||||
* fix test of website
|
||||
|
||||
### Doc
|
||||
|
||||
* devices is a block, not a boolean
|
||||
|
||||
### Feat
|
||||
|
||||
* adds support for OCI manifests ([#316](https://github.com/kreuzwerker/terraform-provider-docker/issues/316))
|
||||
* adds security_opts to container config. ([#308](https://github.com/kreuzwerker/terraform-provider-docker/issues/308))
|
||||
* adds support for init process injection for containers. ([#300](https://github.com/kreuzwerker/terraform-provider-docker/issues/300))
|
||||
|
||||
### Fix
|
||||
|
||||
* changing mounts requires ForceNew ([#314](https://github.com/kreuzwerker/terraform-provider-docker/issues/314))
|
||||
* allow healthcheck to be computed as container can specify ([#312](https://github.com/kreuzwerker/terraform-provider-docker/issues/312))
|
||||
* treat null user as a no-op ([#318](https://github.com/kreuzwerker/terraform-provider-docker/issues/318))
|
||||
* workdir null behavior ([#320](https://github.com/kreuzwerker/terraform-provider-docker/issues/320))
|
||||
|
||||
### Style
|
||||
|
||||
* format with gofumpt
|
||||
|
||||
### Pull Requests
|
||||
|
||||
* Merge pull request [#33](https://github.com/kreuzwerker/terraform-provider-docker/issues/33) from brandonros/patch-1
|
||||
* Merge pull request [#11](https://github.com/kreuzwerker/terraform-provider-docker/issues/11) from suzuki-shunsuke/format-with-gofumpt
|
||||
* Merge pull request [#26](https://github.com/kreuzwerker/terraform-provider-docker/issues/26) from kreuzwerker/ci/fix-website-ci
|
||||
* Merge pull request [#8](https://github.com/kreuzwerker/terraform-provider-docker/issues/8) from dubo-dubon-duponey/patch1
|
||||
|
||||
|
||||
<a name="v2.8.0"></a>
|
||||
## v2.8.0 (2020-11-11)
|
||||
|
||||
### Chore
|
||||
|
||||
* updates changelog for 2.8.0
|
||||
* removes travis.yml
|
||||
* deactivates travis
|
||||
* removes vendor dir ([#298](https://github.com/kreuzwerker/terraform-provider-docker/issues/298))
|
||||
* bump go 115 ([#297](https://github.com/kreuzwerker/terraform-provider-docker/issues/297))
|
||||
* documentation updates ([#286](https://github.com/kreuzwerker/terraform-provider-docker/issues/286))
|
||||
* updates link syntax ([#287](https://github.com/kreuzwerker/terraform-provider-docker/issues/287))
|
||||
* fix typo ([#292](https://github.com/kreuzwerker/terraform-provider-docker/issues/292))
|
||||
|
||||
### Ci
|
||||
|
||||
* reactivats all workflows
|
||||
* fix website
|
||||
* only run website workflow
|
||||
* exports gopath manually
|
||||
* fix absolute gopath for website
|
||||
* make website check separate workflow
|
||||
* fix workflow names
|
||||
* adds website test to unit test
|
||||
* adds acc test
|
||||
* adds compile
|
||||
* adds go version and goproxy env
|
||||
* enables unit tests for master branch
|
||||
* adds unit test workflow
|
||||
* adds goreleaser and gh action
|
||||
* bumps docker and ubuntu versions ([#241](https://github.com/kreuzwerker/terraform-provider-docker/issues/241))
|
||||
* removes debug option from acc tests
|
||||
* skips test which is flaky only on travis
|
||||
|
||||
### Deps
|
||||
|
||||
* github.com/hashicorp/terraform[@sdk](https://github.com/sdk)-v0.11-with-go-modules Updated via: go get github.com/hashicorp/terraform[@sdk](https://github.com/sdk)-v0.11-with-go-modules and go mod tidy
|
||||
* use go modules for dep mgmt run go mod tidy remove govendor from makefile and travis config set appropriate env vars for go modules
|
||||
|
||||
### Docker
|
||||
|
||||
* improve validation of runtime constraints
|
||||
|
||||
### Docs
|
||||
|
||||
* update container.html.markdown ([#278](https://github.com/kreuzwerker/terraform-provider-docker/issues/278))
|
||||
* update service.html.markdown ([#281](https://github.com/kreuzwerker/terraform-provider-docker/issues/281))
|
||||
* update restart_policy for service. Closes [#228](https://github.com/kreuzwerker/terraform-provider-docker/issues/228)
|
||||
* adds new label structure. Closes [#214](https://github.com/kreuzwerker/terraform-provider-docker/issues/214)
|
||||
* update anchors with -1 suffix ([#178](https://github.com/kreuzwerker/terraform-provider-docker/issues/178))
|
||||
* Fix misspelled words
|
||||
* Fix exported attribute name in docker_registry_image
|
||||
* Fix example for docker_registry_image ([#8308](https://github.com/kreuzwerker/terraform-provider-docker/issues/8308))
|
||||
* provider/docker - network settings attrs
|
||||
|
||||
### Feat
|
||||
|
||||
* conditionally adding port binding ([#293](https://github.com/kreuzwerker/terraform-provider-docker/issues/293)).
|
||||
* adds docker Image build feature ([#283](https://github.com/kreuzwerker/terraform-provider-docker/issues/283))
|
||||
* adds complete support for Docker credential helpers ([#253](https://github.com/kreuzwerker/terraform-provider-docker/issues/253))
|
||||
* Expose IPv6 properties as attributes
|
||||
* allow use of source file instead of content / content_base64 ([#240](https://github.com/kreuzwerker/terraform-provider-docker/issues/240))
|
||||
* supports to update docker_container ([#236](https://github.com/kreuzwerker/terraform-provider-docker/issues/236))
|
||||
* support to import some docker_container's attributes ([#234](https://github.com/kreuzwerker/terraform-provider-docker/issues/234))
|
||||
* adds config file content as plain string ([#232](https://github.com/kreuzwerker/terraform-provider-docker/issues/232))
|
||||
* make UID, GID, & mode for secrets and configs configurable ([#231](https://github.com/kreuzwerker/terraform-provider-docker/issues/231))
|
||||
* adds import for resources ([#196](https://github.com/kreuzwerker/terraform-provider-docker/issues/196))
|
||||
* add container ipc mode. ([#182](https://github.com/kreuzwerker/terraform-provider-docker/issues/182))
|
||||
* adds container working dir ([#181](https://github.com/kreuzwerker/terraform-provider-docker/issues/181))
|
||||
|
||||
### Fix
|
||||
|
||||
* ignores 'remove_volumes' on container import
|
||||
* duplicated buildImage function
|
||||
* port objects with the same internal port but different protocol trigger recreation of container ([#274](https://github.com/kreuzwerker/terraform-provider-docker/issues/274))
|
||||
* panic to migrate schema of docker_container from v1 to v2 ([#271](https://github.com/kreuzwerker/terraform-provider-docker/issues/271)). Closes [#264](https://github.com/kreuzwerker/terraform-provider-docker/issues/264)
|
||||
* pins docker registry for tests to v2.7.0
|
||||
* prevent force recreate of container about some attributes ([#269](https://github.com/kreuzwerker/terraform-provider-docker/issues/269))
|
||||
* service endpoint spec flattening
|
||||
* corrects IPAM config read on the data provider ([#229](https://github.com/kreuzwerker/terraform-provider-docker/issues/229))
|
||||
* replica to 0 in current schema. Closes [#221](https://github.com/kreuzwerker/terraform-provider-docker/issues/221)
|
||||
* label for network and volume after improt
|
||||
* binary upload as base 64 content ([#194](https://github.com/kreuzwerker/terraform-provider-docker/issues/194))
|
||||
* service env truncation for multiple delimiters ([#193](https://github.com/kreuzwerker/terraform-provider-docker/issues/193))
|
||||
* destroy_grace_seconds are considered ([#179](https://github.com/kreuzwerker/terraform-provider-docker/issues/179))
|
||||
|
||||
### Make
|
||||
|
||||
* Add website + website-test targets
|
||||
|
||||
### Provider
|
||||
|
||||
* Ensured Go 1.11 in TravisCI and README provider: Run go fix provider: Run go fmt provider: Encode go version 1.11.5 to .go-version file
|
||||
* Require Go 1.11 in TravisCI and README provider: Run go fix provider: Run go fmt
|
||||
|
||||
### Tests
|
||||
|
||||
* Skip test if swap limit isn't available ([#136](https://github.com/kreuzwerker/terraform-provider-docker/issues/136))
|
||||
* Simplify Dockerfile(s)
|
||||
|
||||
### Vendor
|
||||
|
||||
* github.com/hashicorp/terraform/...[@v0](https://github.com/v0).10.0
|
||||
* Ignore github.com/hashicorp/terraform/backend
|
||||
|
||||
### Website
|
||||
|
||||
* Docs sweep for lists & maps
|
||||
* note on docker
|
||||
* docker docs
|
||||
|
||||
### Pull Requests
|
||||
|
||||
* Merge pull request [#134](https://github.com/kreuzwerker/terraform-provider-docker/issues/134) from terraform-providers/go-modules-2019-03-01
|
||||
* Merge pull request [#135](https://github.com/kreuzwerker/terraform-provider-docker/issues/135) from terraform-providers/t-simplify-dockerfile
|
||||
* Merge pull request [#47](https://github.com/kreuzwerker/terraform-provider-docker/issues/47) from captn3m0/docker-link-warning
|
||||
* Merge pull request [#60](https://github.com/kreuzwerker/terraform-provider-docker/issues/60) from terraform-providers/f-make-website
|
||||
* Merge pull request [#23](https://github.com/kreuzwerker/terraform-provider-docker/issues/23) from JamesLaverack/patch-1
|
||||
* Merge pull request [#18](https://github.com/kreuzwerker/terraform-provider-docker/issues/18) from terraform-providers/vendor-tf-0.10
|
||||
* Merge pull request [#5046](https://github.com/kreuzwerker/terraform-provider-docker/issues/5046) from tpounds/use-built-in-schema-string-hash
|
||||
* Merge pull request [#3761](https://github.com/kreuzwerker/terraform-provider-docker/issues/3761) from ryane/f-provider-docker-improvements
|
||||
* Merge pull request [#3383](https://github.com/kreuzwerker/terraform-provider-docker/issues/3383) from apparentlymart/docker-container-command-docs
|
||||
* Merge pull request [#1564](https://github.com/kreuzwerker/terraform-provider-docker/issues/1564) from nickryand/docker_links
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
@@ -0,0 +1,117 @@
|
||||
<a href="https://docker.com">
|
||||
<img src="https://raw.githubusercontent.com/kreuzwerker/terraform-provider-docker/master/assets/docker-logo.png" alt="Docker logo" title="Docker" align="right" height="100" />
|
||||
</a>
|
||||
<a href="https://terraform.io">
|
||||
<img src="https://raw.githubusercontent.com/kreuzwerker/terraform-provider-docker/master/assets/terraform-logo.png" alt="Terraform logo" title="Terraform" align="right" height="100" />
|
||||
</a>
|
||||
<a href="https://kreuzwerker.de">
|
||||
<img src="https://raw.githubusercontent.com/kreuzwerker/terraform-provider-docker/master/assets/xw-logo.png" alt="Kreuzwerker logo" title="Kreuzwerker" align="right" height="100" />
|
||||
</a>
|
||||
|
||||
# Terraform Provider for Docker
|
||||
|
||||
[](https://github.com/kreuzwerker/terraform-provider-docker/releases)
|
||||
[](https://registry.terraform.io/providers/kreuzwerker/docker)
|
||||
[](https://registry.terraform.io/providers/kreuzwerker/docker/latest/docs)
|
||||
[](https://github.com/kreuzwerker/terraform-provider-docker/blob/main/LICENSE)
|
||||
[](https://github.com/kreuzwerker/terraform-provider-docker/actions)
|
||||
[](https://github.com/kreuzwerker/terraform-provider-docker/actions)
|
||||
[](https://goreportcard.com/report/github.com/kreuzwerker/terraform-provider-docker)
|
||||
|
||||
## Documentation
|
||||
|
||||
The documentation for the provider is available on the [Terraform Registry](https://registry.terraform.io/providers/kreuzwerker/docker/latest/docs).
|
||||
|
||||
Do you want to migrate from `v2.x` to `v3.x`? Please read the [migration guide](docs/v2_v3_migration.md)
|
||||
|
||||
## Example usage
|
||||
|
||||
Take a look at the examples in the [documentation](https://registry.terraform.io/providers/kreuzwerker/docker/3.9.0/docs) of the registry
|
||||
or use the following example:
|
||||
|
||||
|
||||
```hcl
|
||||
# Set the required provider and versions
|
||||
terraform {
|
||||
required_providers {
|
||||
# We recommend pinning to the specific version of the Docker Provider you're using
|
||||
# since new versions are released frequently
|
||||
docker = {
|
||||
source = "kreuzwerker/docker"
|
||||
version = "3.9.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Configure the docker provider
|
||||
provider "docker" {
|
||||
}
|
||||
|
||||
# Create a docker image resource
|
||||
# -> docker pull nginx:latest
|
||||
resource "docker_image" "nginx" {
|
||||
name = "nginx:latest"
|
||||
keep_locally = true
|
||||
}
|
||||
|
||||
# Create a docker container resource
|
||||
# -> same as 'docker run --name nginx -p8080:80 -d nginx:latest'
|
||||
resource "docker_container" "nginx" {
|
||||
name = "nginx"
|
||||
image = docker_image.nginx.image_id
|
||||
|
||||
ports {
|
||||
external = 8080
|
||||
internal = 80
|
||||
}
|
||||
}
|
||||
|
||||
# Or create a service resource
|
||||
# -> same as 'docker service create -d -p 8081:80 --name nginx-service --replicas 2 nginx:latest'
|
||||
resource "docker_service" "nginx_service" {
|
||||
name = "nginx-service"
|
||||
task_spec {
|
||||
container_spec {
|
||||
image = docker_image.nginx.repo_digest
|
||||
}
|
||||
}
|
||||
|
||||
mode {
|
||||
replicated {
|
||||
replicas = 2
|
||||
}
|
||||
}
|
||||
|
||||
endpoint_spec {
|
||||
ports {
|
||||
published_port = 8081
|
||||
target_port = 80
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Building The Provider
|
||||
|
||||
[Go](https://golang.org/doc/install) 1.18.x (to build the provider plugin)
|
||||
|
||||
|
||||
```sh
|
||||
$ git clone git@github.com:kreuzwerker/terraform-provider-docker
|
||||
$ make build
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
The Terraform Docker Provider is the work of many of contributors. We appreciate your help!
|
||||
|
||||
To contribute, please read the contribution guidelines: [Contributing to Terraform - Docker Provider](CONTRIBUTING.md)
|
||||
|
||||
## License
|
||||
|
||||
The Terraform Provider Docker is available to everyone under the terms of the Mozilla Public License Version 2.0. [Take a look the LICENSE file](LICENSE).
|
||||
|
||||
|
||||
## Stargazers over time
|
||||
|
||||
[](https://starchart.cc/kreuzwerker/terraform-provider-docker)
|
||||
Binary file not shown.
123
CLAUDE.md
Normal file
123
CLAUDE.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Common Commands
|
||||
|
||||
All commands run from the repo root unless noted.
|
||||
|
||||
### Validate & format
|
||||
|
||||
```sh
|
||||
make validate # tofu validate on both stacks (Docker root + k8s/)
|
||||
make fmt-check # tofu fmt -check -recursive
|
||||
make fmt-fix # auto-format all .tf files
|
||||
make lint # tflint on both stacks (requires tflint in PATH)
|
||||
```
|
||||
|
||||
### Unit tests (no infrastructure required)
|
||||
|
||||
```sh
|
||||
make test-unit # both stacks
|
||||
make test-unit-docker # Docker stack only (runs from repo root)
|
||||
make test-unit-k8s # K8s stack only (runs from k8s/)
|
||||
tofu test -filter=<run_block_name> # single test by name
|
||||
tofu test -verbose # verbose output
|
||||
```
|
||||
|
||||
### Integration tests (real infrastructure)
|
||||
|
||||
```sh
|
||||
make test-integration-k8s # spins up local k3d cluster, applies, verifies, destroys
|
||||
make test-integration-docker # requires Docker + SSH to localhost
|
||||
```
|
||||
|
||||
### Full fast suite (no infra)
|
||||
|
||||
```sh
|
||||
make test-all # validate + fmt-check + unit tests
|
||||
make clean # remove .terraform dirs and local state files
|
||||
```
|
||||
|
||||
### Manual tofu workflow (local deploy, bypassing CI)
|
||||
|
||||
```sh
|
||||
# Docker stack
|
||||
tofu init -backend=false # or with -backend-config flags for SeaweedFS
|
||||
tofu workspace select myapp || tofu workspace new myapp
|
||||
tofu apply -var-file="apps/myapp.tfvars" -var="ssh_key_path=/tmp/key" -var="db_password=..."
|
||||
|
||||
# K8s stack (always run from k8s/)
|
||||
cd k8s
|
||||
tofu init -backend=false
|
||||
tofu workspace select myapp || tofu workspace new myapp
|
||||
tofu apply -var-file="apps/myapp.tfvars" -var="kubeconfig_path=/tmp/kubeconfig" ...
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
This is a **GitOps multi-stack infrastructure blueprint**. There is no application code — only OpenTofu (Terraform-compatible) HCL and CI/CD pipelines.
|
||||
|
||||
### Two independent stacks
|
||||
|
||||
| Stack | Root dir | Module | What it deploys |
|
||||
|-------|----------|--------|-----------------|
|
||||
| Docker | `/` (repo root) | `modules/app-openresty-pg-redis` | OpenResty + PostgreSQL + Redis on a remote Docker host via SSH |
|
||||
| Kubernetes | `k8s/` | `modules/app-k8s-nodered-rabbitmq` | Node-RED + optional RabbitMQ on a k3s cluster via kubeconfig |
|
||||
|
||||
Each stack is a fully independent OpenTofu root. They share no state, no providers, and no modules.
|
||||
|
||||
### One app = one `.tfvars` file = one workspace
|
||||
|
||||
The CI/CD pipelines detect which `apps/*.tfvars` (or `k8s/apps/*.tfvars`) files were added, changed, or deleted, then run `tofu apply` or `tofu destroy` per file in its own **Tofu workspace**. App name is derived from the filename (`apps/myapp.tfvars` → workspace `myapp`).
|
||||
|
||||
### State backend
|
||||
|
||||
Both `backend.tf` files default to local state (no backend block active). To use SeaweedFS S3, uncomment the `terraform { backend "s3" {} }` block and run `scripts/setup-backend.sh`, or pass `-backend-config` flags manually. The backend key pattern is `apps/<appname>.tfstate` (Docker) and `apps-k8s/<appname>.tfstate` (K8s).
|
||||
|
||||
### Secrets pattern
|
||||
|
||||
**No secrets in the repository.** Three values are always injected at CI runtime from gopass:
|
||||
- Docker stack: `ssh_key_path`, `db_password`, optionally `openresty_git_token`
|
||||
- K8s stack: `kubeconfig_path`, `rabbitmq_password`, optionally `loki_auth_token`
|
||||
|
||||
These are passed as `-var` flags and never appear in `.tfvars` files.
|
||||
|
||||
### Module: `modules/app-openresty-pg-redis`
|
||||
|
||||
Three OpenResty source modes controlled by `openresty_source_type`:
|
||||
- `bind_mount` — mounts an existing path from the remote host
|
||||
- `local_build` — builds a Docker image from a local Dockerfile and sends it over SSH
|
||||
- `git_clone` — clones a repo at container startup; `openresty_git_ref` **must** be a pinned tag or SHA (branch names are rejected by variable validation)
|
||||
|
||||
`environment = "prod"` creates named Docker volumes; `"dev"` uses ephemeral containers.
|
||||
|
||||
### Module: `modules/app-k8s-nodered-rabbitmq`
|
||||
|
||||
Key design decisions:
|
||||
- **`kubectl_manifest` (gavinbunney/kubectl provider)** is used for all Traefik CRDs (`IngressRoute`, `Middleware`) instead of `kubernetes_manifest`. This is intentional — `kubectl_manifest` does not validate CRD schemas at plan time, avoiding failures when the CRDs are not yet installed.
|
||||
- **`traefik_api_group`** variable (default `traefik.io/v1alpha1`) controls which Traefik API version is used. Set to `traefik.containo.us/v1alpha1` for older k3s versions.
|
||||
- **Init container** copies files from a custom image to the app PVC at startup, but **skips `flows.json` and `flows_cred.json`** if they already exist (preserves user-modified Node-RED flows across redeployments).
|
||||
- **Grafana Alloy sidecar** tails `/data/logs/*.log` and ships to Loki. It has its own 100Mi PVC (`<appname>-alloy-wal`) mounted at `/var/lib/alloy/data` to persist WAL across pod restarts.
|
||||
- **RabbitMQ** is a StatefulSet (single replica) deployed only when `enable_rabbitmq = true`.
|
||||
|
||||
### Test files
|
||||
|
||||
- `tests/docker_validation.tftest.hcl` — Docker stack unit tests using `mock_provider "docker"`
|
||||
- `k8s/tests/k8s_validation.tftest.hcl` — K8s stack unit tests using `mock_provider "kubernetes"` and `mock_provider "kubectl"`
|
||||
|
||||
### CI/CD
|
||||
|
||||
Two equivalent implementations — use whichever matches your git host:
|
||||
- **Gitea Actions**: `.gitea/workflows/{test,deploy,deploy-k8s}.yml`
|
||||
- **GitLab CI**: `.gitlab-ci.yml` (root entry point) + `.gitlab/workflows/{test,deploy,deploy-k8s}.gitlab-ci.yml`
|
||||
|
||||
Key difference: Gitea uses dynamic matrix jobs (one job per changed tfvars). GitLab CI uses a `for` loop in a single job (`CI_COMMIT_BEFORE_SHA` + `git show` to recover deleted tfvars without checking out the previous commit).
|
||||
|
||||
## Important constraints
|
||||
|
||||
- `openresty_git_ref` has a built-in validation that rejects: `main`, `master`, `develop`, `dev`, `staging`, `HEAD`, `latest`, `trunk`. Always use a version tag or commit SHA.
|
||||
- `environment` only accepts `"prod"` or `"dev"`.
|
||||
- `openresty_source_type` only accepts `"bind_mount"`, `"local_build"`, or `"git_clone"`.
|
||||
- The K8s stack **must be run from the `k8s/` directory** (it references `../modules/`).
|
||||
- `tofu init -backend=false` is required for unit tests and validate — both stacks' `backend.tf` files have the backend block commented out by default.
|
||||
174
Makefile
Normal file
174
Makefile
Normal file
@@ -0,0 +1,174 @@
|
||||
.PHONY: help validate fmt fmt-fix test-unit test-unit-docker test-unit-k8s \
|
||||
test-integration-docker test-integration-k8s test-all clean
|
||||
|
||||
TOFU ?= tofu
|
||||
TFLINT ?= tflint
|
||||
K3D ?= k3d
|
||||
DOCKER ?= docker
|
||||
|
||||
# Names used for local integration test clusters / containers
|
||||
TEST_K3D_CLUSTER ?= tofu-test
|
||||
TEST_DOCKER_NET ?= tofu-test-net
|
||||
TEST_APP_NAME ?= testapp
|
||||
|
||||
##─────────────────────────────────────────────────────────────────────────────
|
||||
## help Show this message
|
||||
##─────────────────────────────────────────────────────────────────────────────
|
||||
help:
|
||||
@grep -E '^##' Makefile | sed 's/^## //'
|
||||
|
||||
##─────────────────────────────────────────────────────────────────────────────
|
||||
## Static analysis (no infrastructure required)
|
||||
##─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
## validate Run 'tofu validate' on both stacks
|
||||
validate:
|
||||
@echo "── Validating Docker stack ──"
|
||||
$(TOFU) init -backend=false -input=false > /dev/null
|
||||
$(TOFU) validate
|
||||
@echo "── Validating K8s stack ──"
|
||||
cd k8s && $(TOFU) init -backend=false -input=false > /dev/null
|
||||
cd k8s && $(TOFU) validate
|
||||
@echo "All configurations valid."
|
||||
|
||||
## fmt-check Check HCL formatting in all .tf files
|
||||
fmt-check:
|
||||
$(TOFU) fmt -check -recursive
|
||||
|
||||
## fmt-fix Auto-format all .tf files
|
||||
fmt-fix:
|
||||
$(TOFU) fmt -recursive
|
||||
|
||||
## lint Run tflint on both stacks (requires tflint in PATH)
|
||||
lint:
|
||||
@echo "── Linting Docker stack ──"
|
||||
$(TFLINT) --recursive
|
||||
@echo "── Linting K8s stack ──"
|
||||
cd k8s && $(TFLINT) --recursive
|
||||
|
||||
##─────────────────────────────────────────────────────────────────────────────
|
||||
## Unit tests (mocked providers — no real infrastructure)
|
||||
##─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
## test-unit-docker Run Docker stack unit tests (tofu test)
|
||||
test-unit-docker:
|
||||
@echo "── Docker stack unit tests ──"
|
||||
$(TOFU) init -backend=false -input=false > /dev/null
|
||||
$(TOFU) test
|
||||
|
||||
## test-unit-k8s Run K8s stack unit tests (tofu test)
|
||||
test-unit-k8s:
|
||||
@echo "── K8s stack unit tests ──"
|
||||
cd k8s && $(TOFU) init -backend=false -input=false > /dev/null
|
||||
cd k8s && $(TOFU) test
|
||||
|
||||
## test-unit Run all unit tests
|
||||
test-unit: test-unit-docker test-unit-k8s
|
||||
|
||||
##─────────────────────────────────────────────────────────────────────────────
|
||||
## Integration tests (real local infrastructure)
|
||||
##─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
## test-integration-docker Deploy to local Docker, verify, destroy
|
||||
## Requires: Docker running locally with SSH to localhost
|
||||
## SSH key: set DOCKER_TEST_KEY env var (default: ~/.ssh/id_ed25519)
|
||||
DOCKER_TEST_KEY ?= $(HOME)/.ssh/id_ed25519
|
||||
DOCKER_TEST_USER ?= $(shell whoami)
|
||||
|
||||
test-integration-docker:
|
||||
@echo "── Docker integration test ──"
|
||||
@echo "Using local Docker socket via SSH to localhost"
|
||||
$(TOFU) init -backend=false -input=false > /dev/null
|
||||
$(TOFU) workspace select $(TEST_APP_NAME) 2>/dev/null || $(TOFU) workspace new $(TEST_APP_NAME)
|
||||
$(TOFU) apply -auto-approve \
|
||||
-var="ssh_host=localhost" \
|
||||
-var="ssh_user=$(DOCKER_TEST_USER)" \
|
||||
-var="ssh_key_path=$(DOCKER_TEST_KEY)" \
|
||||
-var="app_name=$(TEST_APP_NAME)" \
|
||||
-var="environment=dev" \
|
||||
-var="openresty_source_type=bind_mount" \
|
||||
-var="openresty_remote_config_path=/tmp/tofu-test-openresty" \
|
||||
-var="openresty_external_port=18080" \
|
||||
-var="db_name=testdb" \
|
||||
-var="db_user=testuser" \
|
||||
-var="db_password=testpass"
|
||||
@echo "── Verifying containers are running ──"
|
||||
$(DOCKER) ps --filter "name=$(TEST_APP_NAME)" --format "table {{.Names}}\t{{.Status}}"
|
||||
@echo "── Tearing down ──"
|
||||
$(TOFU) destroy -auto-approve \
|
||||
-var="ssh_host=localhost" \
|
||||
-var="ssh_user=$(DOCKER_TEST_USER)" \
|
||||
-var="ssh_key_path=$(DOCKER_TEST_KEY)" \
|
||||
-var="app_name=$(TEST_APP_NAME)" \
|
||||
-var="environment=dev" \
|
||||
-var="openresty_source_type=bind_mount" \
|
||||
-var="openresty_remote_config_path=/tmp/tofu-test-openresty" \
|
||||
-var="openresty_external_port=18080" \
|
||||
-var="db_name=testdb" \
|
||||
-var="db_user=testuser" \
|
||||
-var="db_password=testpass"
|
||||
$(TOFU) workspace select default
|
||||
$(TOFU) workspace delete $(TEST_APP_NAME)
|
||||
@echo "Docker integration test passed."
|
||||
|
||||
## test-integration-k8s Deploy to local k3d cluster, verify, destroy
|
||||
## Requires: k3d and kubectl in PATH
|
||||
test-integration-k8s: _k3d-cluster-create
|
||||
@echo "── K8s integration test ──"
|
||||
$(K3D) kubeconfig get $(TEST_K3D_CLUSTER) > /tmp/test-kubeconfig
|
||||
chmod 600 /tmp/test-kubeconfig
|
||||
cd k8s && $(TOFU) init -backend=false -input=false > /dev/null
|
||||
cd k8s && $(TOFU) workspace select $(TEST_APP_NAME) 2>/dev/null \
|
||||
|| $(TOFU) workspace new $(TEST_APP_NAME)
|
||||
cd k8s && $(TOFU) apply -auto-approve \
|
||||
-var-file="apps/example-nodered.tfvars" \
|
||||
-var="kubeconfig_path=/tmp/test-kubeconfig" \
|
||||
-var="app_name=$(TEST_APP_NAME)" \
|
||||
-var="rabbitmq_password=testpass" \
|
||||
-var="loki_auth_token="
|
||||
@echo "── Verifying K8s resources ──"
|
||||
kubectl --kubeconfig /tmp/test-kubeconfig get all -n $(TEST_APP_NAME)
|
||||
@echo "── Tearing down ──"
|
||||
cd k8s && $(TOFU) destroy -auto-approve \
|
||||
-var-file="apps/example-nodered.tfvars" \
|
||||
-var="kubeconfig_path=/tmp/test-kubeconfig" \
|
||||
-var="app_name=$(TEST_APP_NAME)" \
|
||||
-var="rabbitmq_password=testpass" \
|
||||
-var="loki_auth_token="
|
||||
cd k8s && $(TOFU) workspace select default
|
||||
cd k8s && $(TOFU) workspace delete $(TEST_APP_NAME)
|
||||
$(MAKE) _k3d-cluster-delete
|
||||
rm -f /tmp/test-kubeconfig
|
||||
@echo "K8s integration test passed."
|
||||
|
||||
_k3d-cluster-create:
|
||||
@if ! $(K3D) cluster list | grep -q $(TEST_K3D_CLUSTER); then \
|
||||
echo "Creating k3d cluster '$(TEST_K3D_CLUSTER)'..."; \
|
||||
$(K3D) cluster create $(TEST_K3D_CLUSTER) \
|
||||
--agents 1 \
|
||||
--k3s-arg "--disable=traefik@server:0" \
|
||||
--wait; \
|
||||
echo "Installing Traefik CRDs..."; \
|
||||
kubectl --kubeconfig $$($(K3D) kubeconfig get $(TEST_K3D_CLUSTER)) \
|
||||
apply -f https://raw.githubusercontent.com/traefik/traefik/v2.11/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml; \
|
||||
else \
|
||||
echo "k3d cluster '$(TEST_K3D_CLUSTER)' already exists."; \
|
||||
fi
|
||||
|
||||
_k3d-cluster-delete:
|
||||
$(K3D) cluster delete $(TEST_K3D_CLUSTER) 2>/dev/null || true
|
||||
|
||||
##─────────────────────────────────────────────────────────────────────────────
|
||||
## Combined targets
|
||||
##─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
## test-all Run validate + fmt-check + unit tests
|
||||
test-all: validate fmt-check test-unit
|
||||
@echo "All tests passed."
|
||||
|
||||
## clean Remove local .terraform dirs and state files
|
||||
clean:
|
||||
find . -name '.terraform' -type d -exec rm -rf {} + 2>/dev/null || true
|
||||
find . -name 'terraform.tfstate*' -type f -delete 2>/dev/null || true
|
||||
find . -name '.terraform.lock.hcl' -type f -delete 2>/dev/null || true
|
||||
@echo "Cleaned."
|
||||
958
README.md
Normal file
958
README.md
Normal file
@@ -0,0 +1,958 @@
|
||||
# OpenTofu App Deployment Blueprint
|
||||
|
||||
A gitops-style infrastructure repository for deploying isolated application stacks to remote
|
||||
hosts using [OpenTofu](https://opentofu.org/). Secrets are managed by **gopass**, state is
|
||||
stored in **SeaweedFS** (S3-compatible), and deployments are triggered automatically whenever
|
||||
an app configuration file is added, changed, or deleted. Two CI/CD systems are supported:
|
||||
**Gitea Actions** and **GitLab CI** — both implement identical pipeline logic.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Architecture Overview](#architecture-overview)
|
||||
2. [Repository Structure](#repository-structure)
|
||||
3. [Prerequisites](#prerequisites)
|
||||
4. [State Backend Setup (SeaweedFS)](#state-backend-setup-seaweedfs)
|
||||
5. [Secrets Setup (gopass)](#secrets-setup-gopass)
|
||||
6. [CI/CD Variables](#cicd-variables)
|
||||
- [Gitea Repository Secrets](#gitea-repository-secrets)
|
||||
- [GitLab CI/CD Variables](#gitlab-cicd-variables)
|
||||
7. [Stack: Docker — OpenResty + PostgreSQL + Redis](#stack-docker--openresty--postgresql--redis)
|
||||
- [How It Works](#how-it-works-docker)
|
||||
- [OpenResty Source Modes](#openresty-source-modes)
|
||||
- [Variables Reference](#variables-reference-docker)
|
||||
- [Adding a New Docker App](#adding-a-new-docker-app)
|
||||
8. [Stack: Kubernetes — Node-RED + RabbitMQ](#stack-kubernetes--node-red--rabbitmq)
|
||||
- [How It Works](#how-it-works-kubernetes)
|
||||
- [Init Container Behaviour](#init-container-behaviour)
|
||||
- [Grafana Alloy Sidecar](#grafana-alloy-sidecar)
|
||||
- [RabbitMQ (Optional)](#rabbitmq-optional)
|
||||
- [Traefik Ingress Routes](#traefik-ingress-routes)
|
||||
- [Variables Reference](#variables-reference-kubernetes)
|
||||
- [Adding a New K8s App](#adding-a-new-k8s-app)
|
||||
9. [CI/CD Pipelines](#cicd-pipelines)
|
||||
- [Docker Pipeline](#docker-pipeline)
|
||||
- [Kubernetes Pipeline](#kubernetes-pipeline)
|
||||
- [Destroy on Delete](#destroy-on-delete)
|
||||
- [Gitea vs GitLab differences](#gitea-vs-gitlab-differences)
|
||||
10. [Running Locally](#running-locally)
|
||||
11. [Adding a New Stack Module](#adding-a-new-stack-module)
|
||||
12. [Testing](#testing)
|
||||
13. [Caveats and Known Limitations](#caveats-and-known-limitations)
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
Gitea repo
|
||||
│
|
||||
├── apps/*.tfvars ← Docker app configs
|
||||
└── k8s/apps/*.tfvars ← Kubernetes app configs
|
||||
|
||||
Push to main
|
||||
│
|
||||
├── deploy.yml ← detects apps/*.tfvars changes
|
||||
│ └── tofu apply ← connects to remote Docker host via SSH
|
||||
│
|
||||
└── deploy-k8s.yml ← detects k8s/apps/*.tfvars changes
|
||||
└── tofu apply ← connects to K8s cluster via kubeconfig
|
||||
│
|
||||
└── Per-app workspace in SeaweedFS state bucket
|
||||
```
|
||||
|
||||
Each `.tfvars` file = one app deployment. One Tofu workspace = one isolated state.
|
||||
Secrets are never stored in the repository — they are fetched from **gopass** at pipeline
|
||||
runtime and passed to `tofu apply` via `-var` flags.
|
||||
|
||||
---
|
||||
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
.
|
||||
├── modules/
|
||||
│ ├── app-openresty-pg-redis/ # Docker stack module
|
||||
│ │ ├── variables.tf
|
||||
│ │ ├── main.tf
|
||||
│ │ └── outputs.tf
|
||||
│ └── app-k8s-nodered-rabbitmq/ # Kubernetes stack module
|
||||
│ ├── variables.tf
|
||||
│ ├── main.tf
|
||||
│ └── outputs.tf
|
||||
│
|
||||
├── apps/ # Docker app configs (one file per app)
|
||||
│ ├── example-dev.tfvars
|
||||
│ └── example-prod.tfvars
|
||||
│
|
||||
├── main.tf # Docker stack root: SSH provider + module call
|
||||
├── variables.tf # Docker stack root variables
|
||||
├── backend.tf # State backend options
|
||||
│
|
||||
├── tests/
|
||||
│ └── docker_validation.tftest.hcl # Docker stack unit tests (tofu test)
|
||||
│
|
||||
├── k8s/ # Kubernetes stack root
|
||||
│ ├── main.tf # K8s provider + module call
|
||||
│ ├── variables.tf
|
||||
│ ├── backend.tf
|
||||
│ ├── apps/ # K8s app configs (one file per app)
|
||||
│ │ └── example-nodered.tfvars
|
||||
│ └── tests/
|
||||
│ └── k8s_validation.tftest.hcl # K8s stack unit tests (tofu test)
|
||||
│
|
||||
├── scripts/
|
||||
│ └── setup-backend.sh # One-time SeaweedFS backend setup
|
||||
│
|
||||
├── Makefile # Test runner (validate, test-unit, test-integration)
|
||||
│
|
||||
├── .gitea/
|
||||
│ └── workflows/
|
||||
│ ├── deploy.yml # Docker CI/CD pipeline (Gitea Actions)
|
||||
│ ├── deploy-k8s.yml # Kubernetes CI/CD pipeline (Gitea Actions)
|
||||
│ └── test.yml # Test pipeline (Gitea Actions)
|
||||
│
|
||||
└── .gitlab/
|
||||
├── .gitlab-ci.yml # Root entry point (stages + includes)
|
||||
└── workflows/
|
||||
├── deploy.gitlab-ci.yml # Docker CI/CD pipeline (GitLab CI)
|
||||
├── deploy-k8s.gitlab-ci.yml # Kubernetes CI/CD pipeline (GitLab CI)
|
||||
└── test.gitlab-ci.yml # Test pipeline (GitLab CI)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Tools (CI runner and local workstation)
|
||||
|
||||
| Tool | Minimum version | Purpose |
|
||||
|------|-----------------|---------|
|
||||
| OpenTofu | 1.9.0 | Infrastructure provisioning |
|
||||
| gopass | latest | Secret management |
|
||||
| GPG | 2.x | gopass store encryption |
|
||||
| Docker | 24+ | Remote target host |
|
||||
| k3s / Kubernetes | 1.27+ | K8s target cluster |
|
||||
|
||||
### Remote Docker Host
|
||||
|
||||
- SSH daemon running, key-based auth configured
|
||||
- Docker daemon running and accessible to the deploy user
|
||||
- No additional Docker daemon configuration needed — the Tofu Docker provider
|
||||
connects over SSH
|
||||
|
||||
### Kubernetes Cluster
|
||||
|
||||
- k3s 1.27+ (or any conformant cluster)
|
||||
- Traefik installed with CRD support (`traefik.io/v1alpha1`)
|
||||
— pre-installed on k3s by default
|
||||
- A `StorageClass` available for PVCs (k3s default: `local-path`)
|
||||
- Loki instance reachable from within the cluster (for Alloy log forwarding)
|
||||
|
||||
---
|
||||
|
||||
## State Backend Setup (SeaweedFS)
|
||||
|
||||
OpenTofu state is stored in a SeaweedFS S3 bucket. Each app gets its own state file,
|
||||
isolated by Tofu workspace.
|
||||
|
||||
### 1. Create the state bucket (once)
|
||||
|
||||
```sh
|
||||
weed shell
|
||||
> s3.bucket.create -name tofu-state
|
||||
```
|
||||
|
||||
### 2. Create S3 access credentials in SeaweedFS
|
||||
|
||||
Add an IAM user with read/write access to the `tofu-state` bucket and note the
|
||||
access key and secret key.
|
||||
|
||||
### 3. State key layout
|
||||
|
||||
| Stack | State key pattern |
|
||||
|-------|------------------|
|
||||
| Docker | `apps/<appname>.tfstate` |
|
||||
| Kubernetes | `apps-k8s/<appname>.tfstate` |
|
||||
|
||||
### 4. Initialise the backend (automated)
|
||||
|
||||
```sh
|
||||
chmod +x scripts/setup-backend.sh
|
||||
./scripts/setup-backend.sh
|
||||
```
|
||||
|
||||
The script prompts for the endpoint and credentials, enables the `backend "s3" {}` block
|
||||
in both `backend.tf` files, and runs `tofu init` for both stacks.
|
||||
|
||||
### 4a. Manual initialisation
|
||||
|
||||
Docker stack:
|
||||
```sh
|
||||
tofu init \
|
||||
-backend-config="bucket=tofu-state" \
|
||||
-backend-config="key=apps/<appname>.tfstate" \
|
||||
-backend-config="endpoint=http://seaweedfs.example.com:8333" \
|
||||
-backend-config="region=us-east-1" \
|
||||
-backend-config="force_path_style=true"
|
||||
```
|
||||
|
||||
Kubernetes stack (run from `k8s/`):
|
||||
```sh
|
||||
cd k8s/
|
||||
tofu init \
|
||||
-backend-config="bucket=tofu-state" \
|
||||
-backend-config="key=apps-k8s/<appname>.tfstate" \
|
||||
-backend-config="endpoint=http://seaweedfs.example.com:8333" \
|
||||
-backend-config="region=us-east-1" \
|
||||
-backend-config="force_path_style=true"
|
||||
```
|
||||
|
||||
Uncomment the `backend "s3" {}` block in the respective `backend.tf` before running `init`.
|
||||
|
||||
---
|
||||
|
||||
## Secrets Setup (gopass)
|
||||
|
||||
All sensitive values are stored in gopass and fetched at CI/CD runtime. Nothing sensitive
|
||||
ever touches the repository.
|
||||
|
||||
### Store layout convention
|
||||
|
||||
```
|
||||
gopass store
|
||||
│
|
||||
├── infra/
|
||||
│ ├── ssh-keys/
|
||||
│ │ └── <appname> # SSH private key for the remote Docker host
|
||||
│ └── kubeconfigs/
|
||||
│ └── <appname> # kubeconfig for the target K8s cluster
|
||||
│
|
||||
└── apps/
|
||||
└── <appname>/
|
||||
├── db_password # PostgreSQL password (Docker stack)
|
||||
├── git_token # Git token for private OpenResty repos (optional)
|
||||
├── rabbitmq_password # RabbitMQ password (K8s stack)
|
||||
└── loki_token # Loki bearer token (K8s stack, optional)
|
||||
```
|
||||
|
||||
### Adding a secret
|
||||
|
||||
```sh
|
||||
gopass insert infra/ssh-keys/myapp
|
||||
gopass insert apps/myapp/db_password
|
||||
```
|
||||
|
||||
### Key rotation
|
||||
|
||||
Update the secret in gopass and re-run the pipeline. The next `tofu apply` will pick up
|
||||
the new value and update the K8s Secret or re-create the container with the new env var.
|
||||
|
||||
For SSH keys specifically: update `infra/ssh-keys/<appname>` in gopass and update the
|
||||
`authorized_keys` on the remote host before the next pipeline run.
|
||||
|
||||
---
|
||||
|
||||
## CI/CD Variables
|
||||
|
||||
The same five values are required regardless of which CI/CD system you use.
|
||||
|
||||
### Gitea Repository Secrets
|
||||
|
||||
Configure in **Gitea → Repository → Settings → Secrets**.
|
||||
|
||||
| Secret | Purpose |
|
||||
|--------|---------|
|
||||
| `GOPASS_GPG_KEY` | GPG private key (armored) to decrypt the gopass store |
|
||||
| `GOPASS_STORE_REPO` | Git URL of the gopass password store repository |
|
||||
| `SEAWEED_S3_ENDPOINT` | SeaweedFS S3 endpoint, e.g. `http://seaweedfs.example.com:8333` |
|
||||
| `SEAWEED_ACCESS_KEY` | SeaweedFS S3 access key |
|
||||
| `SEAWEED_SECRET_KEY` | SeaweedFS S3 secret key (mark as masked) |
|
||||
|
||||
### GitLab CI/CD Variables
|
||||
|
||||
Configure in **GitLab → Project → Settings → CI/CD → Variables**. Mark sensitive values
|
||||
as **Masked** so they are redacted from job logs.
|
||||
|
||||
| Variable | Masked | Purpose |
|
||||
|----------|--------|---------|
|
||||
| `GOPASS_GPG_KEY` | yes | GPG private key (armored) to decrypt the gopass store |
|
||||
| `GOPASS_STORE_REPO` | no | Git URL of the gopass password store repository |
|
||||
| `SEAWEED_S3_ENDPOINT` | no | SeaweedFS S3 endpoint, e.g. `http://seaweedfs.example.com:8333` |
|
||||
| `SEAWEED_ACCESS_KEY` | yes | SeaweedFS S3 access key |
|
||||
| `SEAWEED_SECRET_KEY` | yes | SeaweedFS S3 secret key |
|
||||
|
||||
> **Note:** GitLab CI variables are plain environment variables — no `${{ secrets.X }}`
|
||||
> syntax is needed. The pipelines reference them directly as `$VARIABLE_NAME`.
|
||||
|
||||
---
|
||||
|
||||
## Stack: Docker — OpenResty + PostgreSQL + Redis
|
||||
|
||||
Deploys an isolated application stack to a **remote Docker host** over SSH. Each app gets:
|
||||
|
||||
- A dedicated Docker **bridge network** (containers cannot reach other apps)
|
||||
- An **OpenResty** container as the public-facing frontend (configurable source)
|
||||
- A **PostgreSQL** container (internal only)
|
||||
- A **Redis** container (internal only)
|
||||
- Named Docker **volumes** in `prod`, ephemeral containers in `dev`
|
||||
|
||||
### How It Works (Docker)
|
||||
|
||||
```
|
||||
Remote Docker host
|
||||
└── <appname>-network (bridge)
|
||||
├── <appname>-openresty → port <external> on host
|
||||
├── <appname>-postgres (internal only)
|
||||
└── <appname>-redis (internal only)
|
||||
```
|
||||
|
||||
OpenResty receives all external traffic. It communicates with Postgres and Redis using
|
||||
their container names as hostnames (Docker DNS resolution within the network).
|
||||
Environment variables `POSTGRES_HOST`, `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD`,
|
||||
`REDIS_HOST` and `APP_NAME` are injected into the OpenResty container and are accessible
|
||||
from Lua code via `os.getenv()`.
|
||||
|
||||
### OpenResty Source Modes
|
||||
|
||||
The `openresty_source_type` variable controls how the OpenResty config and Lua code
|
||||
are provided. Choose one mode per app.
|
||||
|
||||
#### `bind_mount` — mount from remote host filesystem
|
||||
|
||||
The config directory already exists on the remote server (managed separately via rsync,
|
||||
Ansible, etc.). It is mounted read-only into the container.
|
||||
|
||||
```hcl
|
||||
openresty_source_type = "bind_mount"
|
||||
openresty_remote_config_path = "/opt/apps/myapp/openresty"
|
||||
```
|
||||
|
||||
- The path must exist on the **remote host** before `tofu apply`
|
||||
- Content mounted to `/usr/local/openresty/nginx/conf` inside the container
|
||||
- Best for: quick iteration, config managed outside Tofu
|
||||
|
||||
#### `local_build` — build Docker image from a local Dockerfile
|
||||
|
||||
The build context (directory containing a `Dockerfile`) is transferred from the machine
|
||||
running OpenTofu to the remote Docker daemon over SSH, and built there. The image is
|
||||
stored locally on the remote host.
|
||||
|
||||
```hcl
|
||||
openresty_source_type = "local_build"
|
||||
openresty_local_build_context = "./openresty"
|
||||
openresty_dockerfile = "Dockerfile"
|
||||
```
|
||||
|
||||
- The Dockerfile must produce a valid OpenResty image
|
||||
- Image rebuilds automatically when any file in the context directory changes
|
||||
(detected via SHA-1 hash of all files in the context)
|
||||
- Best for: self-contained deployments, full control over the image
|
||||
|
||||
#### `git_clone` — clone a git repo at container startup
|
||||
|
||||
The standard OpenResty Alpine image starts, installs git via `apk`, clones the specified
|
||||
repository at the pinned ref, then starts OpenResty with the cloned directory as its
|
||||
config prefix.
|
||||
|
||||
```hcl
|
||||
openresty_source_type = "git_clone"
|
||||
openresty_git_repo = "https://gitea.example.com/org/myapp-openresty.git"
|
||||
openresty_git_ref = "v1.4.2"
|
||||
```
|
||||
|
||||
- **Always pin to a tag or full commit SHA** — never use a branch name in production,
|
||||
as it would cause unintended updates on container restarts
|
||||
- The repository must contain an `openresty/` subdirectory with a valid `nginx.conf`
|
||||
- For private repositories, the auth token is supplied at CI runtime from gopass
|
||||
(never stored in the tfvars file)
|
||||
- Best for: production deployments with git-based config versioning
|
||||
|
||||
### Variables Reference (Docker)
|
||||
|
||||
#### Connection
|
||||
|
||||
| Variable | Type | Required | Description |
|
||||
|----------|------|----------|-------------|
|
||||
| `ssh_host` | string | yes | Hostname or IP of the remote Docker host |
|
||||
| `ssh_user` | string | yes | SSH user on the remote host |
|
||||
| `ssh_key_path` | string | yes | Path to SSH private key on the CI runner (from gopass) |
|
||||
|
||||
#### App
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `app_name` | string | — | Unique app name, used as prefix for all resource names |
|
||||
| `environment` | string | `"dev"` | `"prod"` = persistent volumes, `"dev"` = ephemeral |
|
||||
|
||||
#### OpenResty
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `openresty_source_type` | string | — | `"bind_mount"`, `"local_build"`, or `"git_clone"` |
|
||||
| `openresty_image` | string | `openresty/openresty:1.25.3-alpine` | Base image (bind_mount / git_clone) |
|
||||
| `openresty_external_port` | number | — | Port exposed on the remote host |
|
||||
| `openresty_remote_config_path` | string | `""` | Remote host path (bind_mount only) |
|
||||
| `openresty_local_build_context` | string | `""` | Local Dockerfile context path (local_build only) |
|
||||
| `openresty_dockerfile` | string | `"Dockerfile"` | Dockerfile filename (local_build only) |
|
||||
| `openresty_git_repo` | string | `""` | Git repo URL (git_clone only) |
|
||||
| `openresty_git_ref` | string | `""` | Git ref — tag, branch, or SHA (git_clone only) |
|
||||
| `openresty_git_token` | string | `""` | Auth token for private repos (git_clone only, sensitive) |
|
||||
|
||||
#### Database & Cache
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `db_name` | string | — | PostgreSQL database name |
|
||||
| `db_user` | string | — | PostgreSQL user |
|
||||
| `db_password` | string | — | PostgreSQL password (sensitive, from gopass) |
|
||||
| `postgres_image` | string | `postgres:16-alpine` | PostgreSQL image |
|
||||
| `redis_image` | string | `redis:7-alpine` | Redis image |
|
||||
|
||||
### Adding a New Docker App
|
||||
|
||||
1. Copy `apps/example-dev.tfvars` or `apps/example-prod.tfvars` to `apps/<appname>.tfvars`
|
||||
2. Fill in all required variables. Leave `ssh_key_path`, `db_password`, and
|
||||
`openresty_git_token` commented out — they are injected by CI/CD from gopass
|
||||
3. Add secrets to gopass:
|
||||
```sh
|
||||
gopass insert infra/ssh-keys/<appname>
|
||||
gopass insert apps/<appname>/db_password
|
||||
gopass insert apps/<appname>/git_token # only if using git_clone with a private repo
|
||||
```
|
||||
4. Commit and push `apps/<appname>.tfvars` to `main`
|
||||
5. The `deploy.yml` pipeline detects the new file and runs `tofu apply`
|
||||
|
||||
---
|
||||
|
||||
## Stack: Kubernetes — Node-RED + RabbitMQ
|
||||
|
||||
Deploys an application pod to a **Kubernetes cluster** (tested on k3s). Each app gets:
|
||||
|
||||
- A dedicated **Namespace**
|
||||
- A **Deployment** with three containers: app, init, and Grafana Alloy sidecar
|
||||
- A **PersistentVolumeClaim** for the app data directory (`/data`)
|
||||
- A **Service** (ClusterIP) for the app
|
||||
- **Traefik IngressRoute** + **StripPrefix Middleware** for path-based routing
|
||||
- A **K8s Secret** for all sensitive values
|
||||
- A **ConfigMap** with the generated Grafana Alloy River config
|
||||
- An optional **RabbitMQ StatefulSet** with its own PVC, headless service, ClusterIP
|
||||
service, and management UI IngressRoute
|
||||
|
||||
### How It Works (Kubernetes)
|
||||
|
||||
```
|
||||
Namespace: <appname>
|
||||
│
|
||||
├── Deployment: <appname>
|
||||
│ ├── init container ← copies defaults to PVC, preserves flows.json
|
||||
│ ├── app container ← Node-RED (or any image), mounts PVC at /data
|
||||
│ └── alloy sidecar ← tails /data/logs/*.log → Loki
|
||||
│
|
||||
├── PVC: <appname>-data ← shared between init, app, and alloy
|
||||
├── Service: <appname> ← ClusterIP port 1880 (configurable)
|
||||
├── Middleware: strip-prefix
|
||||
├── IngressRoute: <appname> ← PathPrefix(/<appname>) → strips prefix → Service
|
||||
│
|
||||
└── (optional) StatefulSet: <appname>-rabbitmq
|
||||
├── Service: <appname>-rabbitmq-headless (for StatefulSet DNS)
|
||||
├── Service: <appname>-rabbitmq (ClusterIP, AMQP + management)
|
||||
├── Middleware: rabbitmq-strip-prefix
|
||||
└── IngressRoute: <appname>-rabbitmq ← PathPrefix(/rabbitmq) → port 15672
|
||||
```
|
||||
|
||||
### Init Container Behaviour
|
||||
|
||||
The init container runs before the app starts on every pod creation. Its purpose is to
|
||||
populate the app's PVC with default configuration files and Node-RED modules from a
|
||||
custom image, while protecting the user's flows from being overwritten.
|
||||
|
||||
**Logic:**
|
||||
|
||||
```
|
||||
for each file in <init_data_src_path>/ (inside the init image):
|
||||
if file is flows.json or flows_cred.json:
|
||||
if file already exists on the PVC → skip (preserve user flows)
|
||||
else → copy (first-time setup)
|
||||
else:
|
||||
copy unconditionally (always update settings, modules, etc.)
|
||||
```
|
||||
|
||||
This means:
|
||||
- **First deployment**: all files including flows are copied from the init image
|
||||
- **Redeployments / updates**: settings and modules are updated, flows are never touched
|
||||
- **Init image updates**: new modules and config changes are applied automatically on
|
||||
the next pod restart
|
||||
|
||||
The init container image and source path are fully configurable:
|
||||
|
||||
```hcl
|
||||
init_container_image = "gitea.example.com/myorg/myapp-init:v1.2.0"
|
||||
init_data_src_path = "/app-data"
|
||||
```
|
||||
|
||||
**Expected init image layout:**
|
||||
|
||||
```
|
||||
/app-data/
|
||||
├── settings.js ← Node-RED settings
|
||||
├── package.json ← module dependencies
|
||||
└── nodes/ ← custom node modules
|
||||
```
|
||||
|
||||
### Grafana Alloy Sidecar
|
||||
|
||||
An Alloy container runs alongside the app in the same pod, sharing the app PVC (read-only).
|
||||
It tails all `*.log` files under `/data/logs/` and forwards them to Loki.
|
||||
|
||||
Alloy configuration is generated automatically as a Kubernetes ConfigMap. The Loki
|
||||
endpoint and bearer token are configurable variables:
|
||||
|
||||
```hcl
|
||||
loki_endpoint = "http://loki.monitoring.svc:3100/loki/api/v1/push"
|
||||
loki_auth_token = "" # leave empty for unauthenticated Loki; supplied by CI/CD if set
|
||||
```
|
||||
|
||||
The bearer token is never written to the ConfigMap — it is stored in the K8s Secret and
|
||||
read by Alloy at runtime via `env("LOKI_AUTH_TOKEN")`.
|
||||
|
||||
**Log streams include the external labels `app` and `env`**, making it easy to filter
|
||||
in Grafana.
|
||||
|
||||
The app must write logs to `/data/logs/<filename>.log`. Node-RED can be configured to
|
||||
do this via the `settings.js` logging section.
|
||||
|
||||
### RabbitMQ (Optional)
|
||||
|
||||
Enabled by setting `enable_rabbitmq = true`. Deploys a single-node RabbitMQ StatefulSet
|
||||
using the official `rabbitmq:*-management-alpine` image (no Bitnami dependency).
|
||||
|
||||
```hcl
|
||||
enable_rabbitmq = true
|
||||
rabbitmq_user = "myapp"
|
||||
rabbitmq_vhost = "myapp"
|
||||
rabbitmq_pvc_size = "2Gi"
|
||||
rabbitmq_path_prefix = "/myapp-mq"
|
||||
# rabbitmq_password supplied at CI runtime from gopass
|
||||
```
|
||||
|
||||
The app container receives `RABBITMQ_HOST`, `RABBITMQ_USER`, `RABBITMQ_PASSWORD`, and
|
||||
`RABBITMQ_VHOST` as environment variables. The in-cluster AMQP URL is also available as
|
||||
a Tofu output:
|
||||
|
||||
```
|
||||
amqp://<appname>-rabbitmq.<namespace>.svc:5672/<vhost>
|
||||
```
|
||||
|
||||
The management UI (port 15672) is exposed via a dedicated Traefik IngressRoute at
|
||||
the configured path prefix.
|
||||
|
||||
### Traefik Ingress Routes
|
||||
|
||||
Both the app and the RabbitMQ management UI use Traefik CRDs (`traefik.io/v1alpha1`).
|
||||
The `StripPrefix` middleware removes the path prefix before forwarding to the backend,
|
||||
so the app receives requests at `/` regardless of the external path.
|
||||
|
||||
```
|
||||
External request: GET /mynodered/flows
|
||||
Traefik strips /mynodered
|
||||
App receives: GET /flows
|
||||
```
|
||||
|
||||
The Traefik entrypoint (default: `"web"`) is configurable per app via `traefik_entrypoint`.
|
||||
|
||||
> **k3s compatibility note:** k3s 1.27+ ships Traefik with `traefik.io/v1alpha1` CRDs.
|
||||
> For older k3s versions using `traefik.containo.us`, update the `apiVersion` in
|
||||
> [modules/app-k8s-nodered-rabbitmq/main.tf](modules/app-k8s-nodered-rabbitmq/main.tf)
|
||||
> on the `kubernetes_manifest` resources.
|
||||
|
||||
### Variables Reference (Kubernetes)
|
||||
|
||||
#### Connection
|
||||
|
||||
| Variable | Type | Required | Description |
|
||||
|----------|------|----------|-------------|
|
||||
| `kubeconfig_path` | string | yes | Path to kubeconfig on CI runner (from gopass) |
|
||||
|
||||
#### App
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `app_name` | string | — | Unique app name, becomes K8s namespace and resource prefix |
|
||||
| `environment` | string | `"dev"` | Environment label on Loki log streams |
|
||||
| `app_image` | string | — | Container image for the application |
|
||||
| `app_port` | number | `1880` | Internal container port |
|
||||
|
||||
#### Init Container
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `init_container_image` | string | — | Init image containing default data files |
|
||||
| `init_data_src_path` | string | `"/app-data"` | Path inside the init image with default files |
|
||||
|
||||
#### Storage
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `storage_class` | string | `""` | K8s StorageClass (empty = cluster default, k3s: `local-path`) |
|
||||
| `app_pvc_size` | string | `"2Gi"` | Size of the app data PVC |
|
||||
|
||||
#### Ingress
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `app_path_prefix` | string | — | Traefik path prefix, e.g. `"/myapp"` |
|
||||
| `traefik_entrypoint` | string | `"web"` | Traefik entryPoint name |
|
||||
|
||||
#### Grafana Alloy
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `alloy_image` | string | `grafana/alloy:v1.5.0` | Alloy container image |
|
||||
| `loki_endpoint` | string | — | Loki push API URL |
|
||||
| `loki_auth_token` | string | `""` | Loki bearer token (sensitive, from gopass) |
|
||||
|
||||
#### RabbitMQ
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `enable_rabbitmq` | bool | `false` | Deploy RabbitMQ StatefulSet |
|
||||
| `rabbitmq_image` | string | `rabbitmq:3.13-management-alpine` | RabbitMQ image |
|
||||
| `rabbitmq_user` | string | `"guest"` | RabbitMQ default user |
|
||||
| `rabbitmq_password` | string | `""` | RabbitMQ password (sensitive, from gopass) |
|
||||
| `rabbitmq_vhost` | string | `"/"` | RabbitMQ default virtual host |
|
||||
| `rabbitmq_pvc_size` | string | `"2Gi"` | Size of the RabbitMQ data PVC |
|
||||
| `rabbitmq_path_prefix` | string | `"/rabbitmq"` | Traefik path prefix for the management UI |
|
||||
|
||||
### Adding a New K8s App
|
||||
|
||||
1. Copy `k8s/apps/example-nodered.tfvars` to `k8s/apps/<appname>.tfvars`
|
||||
2. Fill in all required variables. Leave `kubeconfig_path`, `loki_auth_token`, and
|
||||
`rabbitmq_password` commented out — they are injected by CI/CD from gopass
|
||||
3. Add secrets to gopass:
|
||||
```sh
|
||||
gopass insert infra/kubeconfigs/<appname>
|
||||
gopass insert apps/<appname>/rabbitmq_password # if enable_rabbitmq = true
|
||||
gopass insert apps/<appname>/loki_token # if Loki requires auth
|
||||
```
|
||||
4. Commit and push `k8s/apps/<appname>.tfvars` to `main`
|
||||
5. The `deploy-k8s.yml` pipeline detects the new file and runs `tofu apply`
|
||||
|
||||
---
|
||||
|
||||
## CI/CD Pipelines
|
||||
|
||||
Both Gitea Actions and GitLab CI implement the same three pipelines:
|
||||
|
||||
| Pipeline | Gitea file | GitLab file | Trigger |
|
||||
|----------|-----------|-------------|---------|
|
||||
| Test | `.gitea/workflows/test.yml` | `.gitlab/workflows/test.gitlab-ci.yml` | all branches + MRs |
|
||||
| Docker deploy | `.gitea/workflows/deploy.yml` | `.gitlab/workflows/deploy.gitlab-ci.yml` | `main` + `apps/*.tfvars` changed |
|
||||
| K8s deploy | `.gitea/workflows/deploy-k8s.yml` | `.gitlab/workflows/deploy-k8s.gitlab-ci.yml` | `main` + `k8s/apps/*.tfvars` changed |
|
||||
|
||||
### Docker Pipeline
|
||||
|
||||
**Trigger:** push to `main` with changes to `apps/*.tfvars`
|
||||
|
||||
**Steps per changed file:**
|
||||
|
||||
1. Resolve app name from filename (`apps/myapp.tfvars` → `myapp`)
|
||||
2. Fetch SSH private key from gopass → `/tmp/deploy_key`
|
||||
3. Fetch `db_password` and optional `git_token` from gopass
|
||||
4. `tofu init` against SeaweedFS backend (state key: `apps/<appname>.tfstate`)
|
||||
5. `tofu workspace select <appname>` or create if new
|
||||
6. `tofu apply` with the tfvars file + runtime secrets via `-var` flags
|
||||
7. Cleanup: `rm /tmp/deploy_key`
|
||||
|
||||
### Kubernetes Pipeline
|
||||
|
||||
**Trigger:** push to `main` with changes to `k8s/apps/*.tfvars`
|
||||
|
||||
**Steps per changed file:**
|
||||
|
||||
1. Resolve app name from filename
|
||||
2. Fetch kubeconfig from gopass → `/tmp/kubeconfig`
|
||||
3. Fetch `rabbitmq_password` and optional `loki_auth_token` from gopass
|
||||
4. `tofu init` (working dir: `k8s/`) against SeaweedFS backend (key: `apps-k8s/<appname>.tfstate`)
|
||||
5. `tofu workspace select <appname>` or create if new
|
||||
6. `tofu apply` (working dir: `k8s/`) with tfvars + runtime secrets
|
||||
7. Cleanup: `rm /tmp/kubeconfig`
|
||||
|
||||
### Destroy on Delete
|
||||
|
||||
Both pipelines include a `destroy` job. When a `.tfvars` file is **deleted** from the
|
||||
repository and pushed to `main`, the pipeline destroys the corresponding app:
|
||||
|
||||
1. Secrets are fetched from gopass as usual
|
||||
2. `tofu destroy` is run using the recovered tfvars and the existing workspace state
|
||||
3. The Tofu workspace is deleted after successful destroy
|
||||
|
||||
> **Note:** This requires the secrets to still exist in gopass at destroy time.
|
||||
> Remove them from gopass only after the destroy pipeline has completed successfully.
|
||||
|
||||
### Gitea vs GitLab differences
|
||||
|
||||
| Behaviour | Gitea Actions | GitLab CI |
|
||||
|-----------|--------------|-----------|
|
||||
| One job per changed file | dynamic matrix (`fromJson`) | loop inside a single job |
|
||||
| Recover deleted tfvars | checkout previous commit (`ref: github.event.before`) | `git show $CI_COMMIT_BEFORE_SHA:$TFVARS` (no extra checkout) |
|
||||
| Secret syntax | `${{ secrets.VAR }}` | `$VAR` (plain env variable) |
|
||||
| Fail-fast behaviour | `fail-fast: false` on matrix | subshell per app, collect exit codes |
|
||||
| Cleanup on failure | `if: always()` step | `after_script:` block |
|
||||
|
||||
---
|
||||
|
||||
## Running Locally
|
||||
|
||||
To apply an app config from your local machine (bypassing CI/CD):
|
||||
|
||||
### Docker stack
|
||||
|
||||
```sh
|
||||
# Fetch the SSH key
|
||||
gopass show -o infra/ssh-keys/myapp > /tmp/deploy_key && chmod 600 /tmp/deploy_key
|
||||
|
||||
# Init and select workspace
|
||||
tofu init # configure backend-config flags as described in the backend section
|
||||
tofu workspace select myapp || tofu workspace new myapp
|
||||
|
||||
# Apply
|
||||
tofu apply \
|
||||
-var-file="apps/myapp.tfvars" \
|
||||
-var="ssh_key_path=/tmp/deploy_key" \
|
||||
-var="db_password=$(gopass show -o apps/myapp/db_password)"
|
||||
|
||||
rm /tmp/deploy_key
|
||||
```
|
||||
|
||||
### Kubernetes stack
|
||||
|
||||
```sh
|
||||
gopass show -o infra/kubeconfigs/myapp > /tmp/kubeconfig && chmod 600 /tmp/kubeconfig
|
||||
|
||||
cd k8s/
|
||||
tofu init # configure backend-config flags
|
||||
tofu workspace select myapp || tofu workspace new myapp
|
||||
|
||||
tofu apply \
|
||||
-var-file="apps/myapp.tfvars" \
|
||||
-var="kubeconfig_path=/tmp/kubeconfig" \
|
||||
-var="rabbitmq_password=$(gopass show -o apps/myapp/rabbitmq_password)"
|
||||
|
||||
rm /tmp/kubeconfig
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Adding a New Stack Module
|
||||
|
||||
The blueprint is designed so that new technology stacks can be added as additional modules
|
||||
without changing the CI/CD pipelines or the existing stacks.
|
||||
|
||||
1. Create a new module directory:
|
||||
```
|
||||
modules/
|
||||
├── app-openresty-pg-redis/ # existing
|
||||
├── app-k8s-nodered-rabbitmq/ # existing
|
||||
└── app-<your-stack>/ # new
|
||||
├── variables.tf
|
||||
├── main.tf
|
||||
└── outputs.tf
|
||||
```
|
||||
|
||||
2. Create a stack root directory with its own `main.tf`, `variables.tf`, and `backend.tf`
|
||||
following the pattern of the existing Docker or K8s roots
|
||||
|
||||
3. Add a new workflow that watches `<stack>/apps/*.tfvars`:
|
||||
- Gitea: `.gitea/workflows/deploy-<stack>.yml` (copy and adapt `deploy.yml`)
|
||||
- GitLab: `.gitlab/workflows/deploy-<stack>.gitlab-ci.yml` + add the `include:` line to `.gitlab-ci.yml`
|
||||
|
||||
4. Add per-app `.tfvars` files under `<stack>/apps/`
|
||||
|
||||
The `apps/*.tfvars` lifecycle (add = deploy, change = update, delete = destroy) and the
|
||||
gopass + SeaweedFS patterns apply identically to all stacks.
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
Testing is organised into four levels. Each level requires more infrastructure but
|
||||
gives stronger confidence. The `Makefile` provides convenient targets for all levels.
|
||||
|
||||
```
|
||||
Level 1 — Static No infrastructure tofu validate, tofu fmt, tflint
|
||||
Level 2 — Unit No infrastructure tofu test (mocked providers)
|
||||
Level 3 — Integration Real local infra Docker or k3d cluster on your machine
|
||||
Level 4 — CI Gitea Actions test.yml runs L1+L2 on every push, L3 on main
|
||||
```
|
||||
|
||||
### Level 1: Static Analysis
|
||||
|
||||
Checks syntax, types, and formatting. Runs in under 10 seconds, no network required.
|
||||
|
||||
```sh
|
||||
make validate # tofu validate on both stacks
|
||||
make fmt-check # tofu fmt -check -recursive
|
||||
make lint # tflint (install separately: https://github.com/terraform-linters/tflint)
|
||||
```
|
||||
|
||||
Or manually:
|
||||
```sh
|
||||
# Docker stack
|
||||
tofu init -backend=false && tofu validate
|
||||
|
||||
# K8s stack
|
||||
cd k8s && tofu init -backend=false && tofu validate
|
||||
```
|
||||
|
||||
### Level 2: Unit Tests (mocked providers)
|
||||
|
||||
Uses the OpenTofu native test framework (`tofu test`) with `mock_provider` blocks.
|
||||
**No real Docker host or Kubernetes cluster is needed.** All provider API calls are
|
||||
intercepted and return mock values.
|
||||
|
||||
**Requires OpenTofu >= 1.7.0.**
|
||||
|
||||
```sh
|
||||
make test-unit # both stacks
|
||||
make test-unit-docker # Docker stack only
|
||||
make test-unit-k8s # K8s stack only
|
||||
```
|
||||
|
||||
#### What is tested
|
||||
|
||||
**Docker stack** ([tests/docker_validation.tftest.hcl](tests/docker_validation.tftest.hcl)):
|
||||
|
||||
| Test | Type |
|
||||
|------|------|
|
||||
| Valid bind_mount / git_clone / local_build configs plan without error | Smoke |
|
||||
| `openresty_git_ref = "main"` is rejected | Validation |
|
||||
| `openresty_git_ref = "master"`, `"develop"`, `"HEAD"`, `"latest"` are rejected | Validation |
|
||||
| `openresty_git_ref = "v2.1.0"` (tag) is accepted | Validation |
|
||||
| `environment = "staging"` is rejected | Validation |
|
||||
| `openresty_source_type = "s3_bucket"` is rejected | Validation |
|
||||
|
||||
**K8s stack** ([k8s/tests/k8s_validation.tftest.hcl](k8s/tests/k8s_validation.tftest.hcl)):
|
||||
|
||||
| Test | Type |
|
||||
|------|------|
|
||||
| Valid plan without RabbitMQ | Smoke |
|
||||
| Valid plan with RabbitMQ enabled | Smoke |
|
||||
| `traefik.io/v1alpha1` and `traefik.containo.us/v1alpha1` both plan | Smoke |
|
||||
| `output.rabbitmq_amqp_url == ""` when RabbitMQ disabled | Assertion |
|
||||
| `output.namespace == var.app_name` | Assertion |
|
||||
| `output.app_url_path == var.app_path_prefix` | Assertion |
|
||||
|
||||
#### Running a single test
|
||||
|
||||
```sh
|
||||
tofu test -filter=reject_git_ref_main
|
||||
```
|
||||
|
||||
#### Verbose output
|
||||
|
||||
```sh
|
||||
tofu test -verbose
|
||||
```
|
||||
|
||||
### Level 3: Integration Tests (real local infrastructure)
|
||||
|
||||
These tests apply real resources, verify them, then destroy. They are the strongest
|
||||
confidence check before deploying to production.
|
||||
|
||||
#### Docker integration test
|
||||
|
||||
Tests the Docker stack against your **local Docker daemon** via SSH to localhost.
|
||||
Requires: Docker running, SSH server on localhost with your key in `authorized_keys`.
|
||||
|
||||
```sh
|
||||
make test-integration-docker
|
||||
|
||||
# Override the SSH key and user if needed:
|
||||
DOCKER_TEST_KEY=~/.ssh/id_ed25519 DOCKER_TEST_USER=deploy make test-integration-docker
|
||||
```
|
||||
|
||||
What happens:
|
||||
1. Creates OpenTofu workspace `testapp`
|
||||
2. `tofu apply` deploys OpenResty + Postgres + Redis to local Docker
|
||||
3. `docker ps` verifies containers are running
|
||||
4. `tofu destroy` tears everything down
|
||||
5. Workspace is deleted
|
||||
|
||||
#### K8s integration test
|
||||
|
||||
Tests the K8s stack against a **local k3d cluster**. Requires: k3d and kubectl in `PATH`.
|
||||
|
||||
```sh
|
||||
make test-integration-k8s
|
||||
```
|
||||
|
||||
What happens:
|
||||
1. Creates a k3d cluster `tofu-test` with Traefik disabled (CRDs installed manually)
|
||||
2. `tofu apply` deploys the example Node-RED app with RabbitMQ
|
||||
3. Verifies namespace, Deployment, Services, PVCs, and IngressRoutes exist
|
||||
4. `tofu destroy` tears everything down
|
||||
5. k3d cluster is deleted
|
||||
|
||||
> **Note on Traefik in k3d**: the test installs Traefik CRDs manually from the
|
||||
> upstream Traefik repo so `kubectl_manifest` resources apply correctly. The Traefik
|
||||
> controller itself is not installed (not needed to test resource creation).
|
||||
|
||||
### Level 4: CI Pipeline
|
||||
|
||||
Two equivalent pipelines are provided — use whichever matches your git host.
|
||||
|
||||
#### Gitea Actions ([.gitea/workflows/test.yml](.gitea/workflows/test.yml))
|
||||
|
||||
| Trigger | Jobs run |
|
||||
|---------|---------|
|
||||
| Push to any branch / PR to `main` | Static (L1) + Unit tests (L2) |
|
||||
| Push to `main` | Static + Unit + K8s integration (L3) |
|
||||
|
||||
#### GitLab CI ([.gitlab/workflows/test.gitlab-ci.yml](.gitlab/workflows/test.gitlab-ci.yml))
|
||||
|
||||
| Trigger | Jobs run |
|
||||
|---------|---------|
|
||||
| Merge request / push to `feature/*` | `static-analysis` + `unit-tests-docker` + `unit-tests-k8s` |
|
||||
| Push to `main` | All of the above + `integration-k8s` |
|
||||
|
||||
Both pipelines follow the same stage dependency graph:
|
||||
|
||||
```
|
||||
static ──► unit-docker ──►
|
||||
└─► unit-k8s ──► integration-k8s (main branch only)
|
||||
```
|
||||
|
||||
`integration-k8s` always cleans up the k3d cluster even if apply or verify fails
|
||||
(Gitea: `if: always()` step; GitLab CI: `after_script:` block).
|
||||
|
||||
### Quick reference
|
||||
|
||||
```sh
|
||||
make test-all # L1 + L2 (fast, no infra needed)
|
||||
make test-integration-k8s # L3 K8s (needs k3d)
|
||||
make test-integration-docker # L3 Docker (needs local Docker + SSH)
|
||||
make clean # remove .terraform dirs and local state files
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Caveats and Known Limitations
|
||||
|
||||
**Single-node RabbitMQ**
|
||||
The RabbitMQ StatefulSet deploys a single replica without clustering configuration.
|
||||
This is appropriate for development and low-traffic production workloads. For
|
||||
high-availability RabbitMQ, use the
|
||||
[RabbitMQ Cluster Operator](https://www.rabbitmq.com/kubernetes/operator/operator-overview)
|
||||
instead of this module's built-in StatefulSet.
|
||||
|
||||
All other original caveats have been resolved:
|
||||
|
||||
| Was | Resolution |
|
||||
|-----|-----------|
|
||||
| Traefik API version hardcoded | `traefik_api_group` variable — default `traefik.io/v1alpha1`, override to `traefik.containo.us/v1alpha1` for older k3s |
|
||||
| `kubernetes_manifest` fails at plan time if CRDs absent | Switched to `kubectl_manifest` (`gavinbunney/kubectl` provider) — no CRD schema validation at plan time |
|
||||
| git_clone accepts mutable branch names silently | `openresty_git_ref` validation rejects `main`, `master`, `develop`, `dev`, `staging`, `HEAD`, `latest`, `trunk` at `tofu validate` time |
|
||||
| Alloy loses log position on pod restart, re-sends duplicates | Dedicated 100Mi PVC (`<appname>-alloy-wal`) mounted at `/var/lib/alloy/data` — WAL persists across restarts |
|
||||
| State backend disabled and manual to enable | `scripts/setup-backend.sh` — interactive script that enables the backend block in both `backend.tf` files and runs `tofu init` |
|
||||
24
apps/example-dev.tfvars
Normal file
24
apps/example-dev.tfvars
Normal file
@@ -0,0 +1,24 @@
|
||||
# ─── Remote Host ──────────────────────────────────────────────────────────────
|
||||
ssh_host = "dev-server.example.com"
|
||||
ssh_user = "deploy"
|
||||
# ssh_key_path is supplied at runtime by CI/CD from gopass — not stored here.
|
||||
|
||||
# ─── App Identity ─────────────────────────────────────────────────────────────
|
||||
app_name = "myapp"
|
||||
environment = "dev" # ephemeral containers, no persistent volumes
|
||||
|
||||
# ─── OpenResty: bind_mount mode ───────────────────────────────────────────────
|
||||
# Mount a directory that already exists on the remote server.
|
||||
# Manage the files there separately (rsync, Ansible, etc.).
|
||||
openresty_source_type = "bind_mount"
|
||||
openresty_remote_config_path = "/opt/apps/myapp/openresty"
|
||||
openresty_external_port = 8080
|
||||
|
||||
# ─── PostgreSQL ───────────────────────────────────────────────────────────────
|
||||
db_name = "myapp_dev"
|
||||
db_user = "myapp"
|
||||
db_password = "changeme-dev" # use gopass / CI secret for real deployments
|
||||
|
||||
# ─── Images (optional overrides) ──────────────────────────────────────────────
|
||||
# postgres_image = "postgres:16-alpine"
|
||||
# redis_image = "redis:7-alpine"
|
||||
27
apps/example-prod.tfvars
Normal file
27
apps/example-prod.tfvars
Normal file
@@ -0,0 +1,27 @@
|
||||
# ─── Remote Host ──────────────────────────────────────────────────────────────
|
||||
ssh_host = "prod-server.example.com"
|
||||
ssh_user = "deploy"
|
||||
# ssh_key_path is supplied at runtime by CI/CD from gopass — not stored here.
|
||||
|
||||
# ─── App Identity ─────────────────────────────────────────────────────────────
|
||||
app_name = "myapp"
|
||||
environment = "prod" # named Docker volumes, data persists across container restarts
|
||||
|
||||
# ─── OpenResty: git_clone mode ────────────────────────────────────────────────
|
||||
# The OpenResty Alpine image starts, installs git via apk, then clones the repo.
|
||||
# Pin to a tag or full commit SHA — never use a branch name in prod.
|
||||
# The repo must contain an 'openresty/' directory with a valid nginx.conf.
|
||||
openresty_source_type = "git_clone"
|
||||
openresty_git_repo = "https://gitea.example.com/myorg/myapp-openresty.git"
|
||||
openresty_git_ref = "v1.4.2" # pinned tag — never 'main' in prod
|
||||
# openresty_git_token is supplied at runtime by CI/CD from gopass — not stored here.
|
||||
openresty_external_port = 80
|
||||
|
||||
# ─── PostgreSQL ───────────────────────────────────────────────────────────────
|
||||
db_name = "myapp_prod"
|
||||
db_user = "myapp"
|
||||
# db_password is supplied at runtime by CI/CD from gopass — not stored here.
|
||||
|
||||
# ─── Images (optional overrides) ──────────────────────────────────────────────
|
||||
# postgres_image = "postgres:16-alpine"
|
||||
# redis_image = "redis:7-alpine"
|
||||
50
backend.tf
Normal file
50
backend.tf
Normal file
@@ -0,0 +1,50 @@
|
||||
# ─── State Backend ────────────────────────────────────────────────────────────
|
||||
#
|
||||
# Each app deployment uses its own Tofu workspace so state is isolated.
|
||||
# The CI/CD pipeline selects (or creates) the workspace named after the app
|
||||
# before running plan/apply.
|
||||
#
|
||||
# OPTION A — SeaweedFS S3 API (recommended for self-hosted Gitea CI/CD)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# SeaweedFS exposes an S3-compatible API (default port 8333).
|
||||
# Create the state bucket once: `weed shell` → `s3.bucket.create -name tofu-state`
|
||||
#
|
||||
# Configure via environment variables in the Gitea runner (no secrets in code):
|
||||
#
|
||||
# AWS_ACCESS_KEY_ID = <seaweedfs-access-key>
|
||||
# AWS_SECRET_ACCESS_KEY = <seaweedfs-secret-key>
|
||||
#
|
||||
# Then initialise with:
|
||||
# tofu init \
|
||||
# -backend-config="bucket=tofu-state" \
|
||||
# -backend-config="key=apps/${APP_NAME}.tfstate" \
|
||||
# -backend-config="endpoint=http://seaweedfs.example.com:8333" \
|
||||
# -backend-config="region=us-east-1" \
|
||||
# -backend-config="force_path_style=true"
|
||||
#
|
||||
# Uncomment to enable:
|
||||
#
|
||||
# terraform {
|
||||
# backend "s3" {}
|
||||
# }
|
||||
|
||||
# OPTION B — HTTP backend (e.g., a custom state server or Gitlab-compatible endpoint)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# tofu init \
|
||||
# -backend-config="address=https://state.example.com/apps/${APP_NAME}" \
|
||||
# -backend-config="lock_address=https://state.example.com/apps/${APP_NAME}/lock" \
|
||||
# -backend-config="unlock_address=https://state.example.com/apps/${APP_NAME}/lock" \
|
||||
# -backend-config="username=${TF_HTTP_USERNAME}" \
|
||||
# -backend-config="password=${TF_HTTP_PASSWORD}"
|
||||
#
|
||||
# Uncomment to enable:
|
||||
#
|
||||
# terraform {
|
||||
# backend "http" {}
|
||||
# }
|
||||
|
||||
# OPTION C — Local backend (default, useful for local development)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# State is stored in terraform.tfstate.d/<workspace>/terraform.tfstate
|
||||
# Commit .gitignore entries for *.tfstate and *.tfstate.backup.
|
||||
# Not suitable for concurrent CI/CD runs.
|
||||
37
k8s/apps/example-nodered.tfvars
Normal file
37
k8s/apps/example-nodered.tfvars
Normal file
@@ -0,0 +1,37 @@
|
||||
# ─── Kubernetes Connection ────────────────────────────────────────────────────
|
||||
# kubeconfig_path is supplied at runtime by CI/CD from gopass — not stored here.
|
||||
|
||||
# ─── App Identity ─────────────────────────────────────────────────────────────
|
||||
app_name = "mynodered"
|
||||
environment = "prod"
|
||||
|
||||
# ─── App Container ────────────────────────────────────────────────────────────
|
||||
app_image = "nodered/node-red:3.1.9"
|
||||
app_port = 1880
|
||||
|
||||
# ─── Init Container ───────────────────────────────────────────────────────────
|
||||
# Custom image containing default settings.js, package.json and node modules.
|
||||
# flows.json and flows_cred.json are preserved if they already exist on the PVC.
|
||||
init_container_image = "gitea.example.com/myorg/mynodered-init:v1.2.0"
|
||||
init_data_src_path = "/app-data"
|
||||
|
||||
# ─── Storage ──────────────────────────────────────────────────────────────────
|
||||
storage_class = "local-path" # k3s default provisioner
|
||||
app_pvc_size = "2Gi"
|
||||
|
||||
# ─── Ingress (Traefik) ────────────────────────────────────────────────────────
|
||||
app_path_prefix = "/mynodered"
|
||||
traefik_entrypoint = "web"
|
||||
|
||||
# ─── Grafana Alloy ────────────────────────────────────────────────────────────
|
||||
loki_endpoint = "http://loki.monitoring.svc:3100/loki/api/v1/push"
|
||||
# loki_auth_token is supplied at runtime by CI/CD from gopass — not stored here.
|
||||
|
||||
# ─── RabbitMQ (optional) ──────────────────────────────────────────────────────
|
||||
enable_rabbitmq = true
|
||||
rabbitmq_image = "rabbitmq:3.13-management-alpine"
|
||||
rabbitmq_user = "mynodered"
|
||||
rabbitmq_vhost = "mynodered"
|
||||
rabbitmq_pvc_size = "2Gi"
|
||||
rabbitmq_path_prefix = "/mynodered-mq"
|
||||
# rabbitmq_password is supplied at runtime by CI/CD from gopass — not stored here.
|
||||
30
k8s/backend.tf
Normal file
30
k8s/backend.tf
Normal file
@@ -0,0 +1,30 @@
|
||||
# ─── State Backend ────────────────────────────────────────────────────────────
|
||||
#
|
||||
# Mirrors the Docker stack backend configuration — same SeaweedFS store,
|
||||
# separate state keys under apps-k8s/.
|
||||
#
|
||||
# OPTION A — SeaweedFS S3 API (recommended)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Initialise with:
|
||||
# cd k8s/
|
||||
# tofu init \
|
||||
# -backend-config="bucket=tofu-state" \
|
||||
# -backend-config="key=apps-k8s/${APP_NAME}.tfstate" \
|
||||
# -backend-config="endpoint=http://seaweedfs.example.com:8333" \
|
||||
# -backend-config="region=us-east-1" \
|
||||
# -backend-config="force_path_style=true"
|
||||
#
|
||||
# Credentials via environment variables on the CI runner:
|
||||
# AWS_ACCESS_KEY_ID = <seaweedfs-access-key>
|
||||
# AWS_SECRET_ACCESS_KEY = <seaweedfs-secret-key>
|
||||
#
|
||||
# Uncomment to enable:
|
||||
#
|
||||
# terraform {
|
||||
# backend "s3" {}
|
||||
# }
|
||||
|
||||
# OPTION B — Local backend (default, for local development)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# State stored at k8s/terraform.tfstate.d/<workspace>/terraform.tfstate
|
||||
# Add k8s/**/.terraform and k8s/terraform.tfstate* to .gitignore.
|
||||
76
k8s/main.tf
Normal file
76
k8s/main.tf
Normal file
@@ -0,0 +1,76 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
kubernetes = {
|
||||
source = "hashicorp/kubernetes"
|
||||
version = "~> 2.31"
|
||||
}
|
||||
kubectl = {
|
||||
source = "gavinbunney/kubectl"
|
||||
version = "~> 1.14"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ─── Providers ────────────────────────────────────────────────────────────────
|
||||
# Both providers connect to the same cluster via the same kubeconfig.
|
||||
# kubectl is used exclusively for Traefik CRD resources (IngressRoute, Middleware)
|
||||
# because it does not validate CRD schemas at plan time, unlike kubernetes_manifest.
|
||||
|
||||
provider "kubernetes" {
|
||||
config_path = var.kubeconfig_path
|
||||
}
|
||||
|
||||
provider "kubectl" {
|
||||
config_path = var.kubeconfig_path
|
||||
}
|
||||
|
||||
# ─── App Deployment ───────────────────────────────────────────────────────────
|
||||
|
||||
module "app" {
|
||||
source = "../modules/app-k8s-nodered-rabbitmq"
|
||||
|
||||
app_name = var.app_name
|
||||
environment = var.environment
|
||||
|
||||
app_image = var.app_image
|
||||
app_port = var.app_port
|
||||
|
||||
init_container_image = var.init_container_image
|
||||
init_data_src_path = var.init_data_src_path
|
||||
|
||||
storage_class = var.storage_class
|
||||
app_pvc_size = var.app_pvc_size
|
||||
|
||||
app_path_prefix = var.app_path_prefix
|
||||
traefik_entrypoint = var.traefik_entrypoint
|
||||
traefik_api_group = var.traefik_api_group
|
||||
|
||||
alloy_image = var.alloy_image
|
||||
loki_endpoint = var.loki_endpoint
|
||||
loki_auth_token = var.loki_auth_token
|
||||
|
||||
enable_rabbitmq = var.enable_rabbitmq
|
||||
rabbitmq_image = var.rabbitmq_image
|
||||
rabbitmq_user = var.rabbitmq_user
|
||||
rabbitmq_password = var.rabbitmq_password
|
||||
rabbitmq_vhost = var.rabbitmq_vhost
|
||||
rabbitmq_pvc_size = var.rabbitmq_pvc_size
|
||||
rabbitmq_path_prefix = var.rabbitmq_path_prefix
|
||||
}
|
||||
|
||||
# ─── Outputs ──────────────────────────────────────────────────────────────────
|
||||
|
||||
output "namespace" {
|
||||
description = "Kubernetes namespace for this app."
|
||||
value = module.app.namespace
|
||||
}
|
||||
|
||||
output "app_url_path" {
|
||||
description = "Path prefix the app is accessible at via Traefik."
|
||||
value = module.app.app_path_prefix
|
||||
}
|
||||
|
||||
output "rabbitmq_amqp_url" {
|
||||
description = "In-cluster AMQP URL (empty if RabbitMQ is disabled)."
|
||||
value = module.app.rabbitmq_amqp_url
|
||||
}
|
||||
122
k8s/tests/k8s_validation.tftest.hcl
Normal file
122
k8s/tests/k8s_validation.tftest.hcl
Normal file
@@ -0,0 +1,122 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# K8s stack unit tests — run with: tofu test (from k8s/ directory)
|
||||
# Requires OpenTofu >= 1.7.0 (mock_provider support)
|
||||
#
|
||||
# Both providers are mocked — no real Kubernetes cluster needed.
|
||||
# Tests cover: variable validation, plan with/without RabbitMQ, Traefik API group.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
mock_provider "kubernetes" {}
|
||||
|
||||
mock_provider "kubectl" {
|
||||
source = "gavinbunney/kubectl"
|
||||
}
|
||||
|
||||
# ─── Shared baseline variables ────────────────────────────────────────────────
|
||||
|
||||
variables {
|
||||
kubeconfig_path = "/tmp/test-kubeconfig"
|
||||
|
||||
app_name = "testapp"
|
||||
environment = "dev"
|
||||
|
||||
app_image = "nodered/node-red:3.1.9"
|
||||
app_port = 1880
|
||||
init_container_image = "gitea.example.com/org/testapp-init:v1.0.0"
|
||||
|
||||
app_path_prefix = "/testapp"
|
||||
loki_endpoint = "http://loki.monitoring.svc:3100/loki/api/v1/push"
|
||||
}
|
||||
|
||||
# ─── Smoke tests: valid configurations plan without error ─────────────────────
|
||||
|
||||
run "valid_minimal_no_rabbitmq" {
|
||||
command = plan
|
||||
# Baseline: no RabbitMQ, default storage class, no Loki auth
|
||||
}
|
||||
|
||||
run "valid_with_rabbitmq" {
|
||||
command = plan
|
||||
variables {
|
||||
enable_rabbitmq = true
|
||||
rabbitmq_user = "testapp"
|
||||
rabbitmq_password = "rabbit-secret"
|
||||
rabbitmq_vhost = "testapp"
|
||||
}
|
||||
}
|
||||
|
||||
run "valid_prod_environment" {
|
||||
command = plan
|
||||
variables {
|
||||
environment = "prod"
|
||||
enable_rabbitmq = true
|
||||
rabbitmq_password = "rabbit-secret"
|
||||
}
|
||||
}
|
||||
|
||||
run "valid_with_loki_auth" {
|
||||
command = plan
|
||||
variables {
|
||||
loki_auth_token = "glc_supersecrettoken"
|
||||
}
|
||||
}
|
||||
|
||||
run "valid_custom_storage_class" {
|
||||
command = plan
|
||||
variables {
|
||||
storage_class = "longhorn"
|
||||
app_pvc_size = "5Gi"
|
||||
}
|
||||
}
|
||||
|
||||
run "valid_websecure_entrypoint" {
|
||||
command = plan
|
||||
variables {
|
||||
traefik_entrypoint = "websecure"
|
||||
}
|
||||
}
|
||||
|
||||
# ─── Traefik API group: both supported values plan without error ───────────────
|
||||
|
||||
run "valid_traefik_api_group_new" {
|
||||
command = plan
|
||||
variables {
|
||||
traefik_api_group = "traefik.io/v1alpha1"
|
||||
}
|
||||
}
|
||||
|
||||
run "valid_traefik_api_group_legacy" {
|
||||
command = plan
|
||||
variables {
|
||||
traefik_api_group = "traefik.containo.us/v1alpha1"
|
||||
}
|
||||
}
|
||||
|
||||
# ─── Output assertions ────────────────────────────────────────────────────────
|
||||
|
||||
run "rabbitmq_amqp_url_empty_when_disabled" {
|
||||
command = plan
|
||||
variables {
|
||||
enable_rabbitmq = false
|
||||
}
|
||||
assert {
|
||||
condition = output.rabbitmq_amqp_url == ""
|
||||
error_message = "AMQP URL should be empty string when RabbitMQ is disabled."
|
||||
}
|
||||
}
|
||||
|
||||
run "namespace_matches_app_name" {
|
||||
command = plan
|
||||
assert {
|
||||
condition = output.namespace == var.app_name
|
||||
error_message = "Namespace should equal app_name."
|
||||
}
|
||||
}
|
||||
|
||||
run "app_url_path_matches_prefix" {
|
||||
command = plan
|
||||
assert {
|
||||
condition = output.app_url_path == var.app_path_prefix
|
||||
error_message = "app_url_path output should match app_path_prefix variable."
|
||||
}
|
||||
}
|
||||
143
k8s/variables.tf
Normal file
143
k8s/variables.tf
Normal file
@@ -0,0 +1,143 @@
|
||||
# ─── Kubernetes Connection ────────────────────────────────────────────────────
|
||||
|
||||
variable "kubeconfig_path" {
|
||||
description = "Absolute path to the kubeconfig file on the CI runner (fetched from gopass at pipeline runtime)."
|
||||
type = string
|
||||
}
|
||||
|
||||
# ─── App Identity ─────────────────────────────────────────────────────────────
|
||||
|
||||
variable "app_name" {
|
||||
description = "Unique name for this app deployment. Becomes the K8s namespace and resource prefix."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "environment" {
|
||||
description = "Deployment environment label (attached to Loki log streams)."
|
||||
type = string
|
||||
default = "dev"
|
||||
}
|
||||
|
||||
# ─── App Container ────────────────────────────────────────────────────────────
|
||||
|
||||
variable "app_image" {
|
||||
description = "Container image for the main application."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "app_port" {
|
||||
description = "Port the app container listens on."
|
||||
type = number
|
||||
default = 1880
|
||||
}
|
||||
|
||||
# ─── Init Container ───────────────────────────────────────────────────────────
|
||||
|
||||
variable "init_container_image" {
|
||||
description = "Image for the init container (must contain default data files at init_data_src_path)."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "init_data_src_path" {
|
||||
description = "Path inside the init container image where default data files live."
|
||||
type = string
|
||||
default = "/app-data"
|
||||
}
|
||||
|
||||
# ─── Storage ──────────────────────────────────────────────────────────────────
|
||||
|
||||
variable "storage_class" {
|
||||
description = "Kubernetes StorageClass for PVCs. Empty string uses cluster default (k3s: local-path)."
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "app_pvc_size" {
|
||||
description = "Storage size for the app data PVC."
|
||||
type = string
|
||||
default = "2Gi"
|
||||
}
|
||||
|
||||
# ─── Ingress ──────────────────────────────────────────────────────────────────
|
||||
|
||||
variable "app_path_prefix" {
|
||||
description = "Traefik URL path prefix for the app (e.g. '/myapp')."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "traefik_api_group" {
|
||||
description = "Traefik CRD API group. k3s >= 1.27: 'traefik.io/v1alpha1'. Older: 'traefik.containo.us/v1alpha1'."
|
||||
type = string
|
||||
default = "traefik.io/v1alpha1"
|
||||
}
|
||||
|
||||
variable "traefik_entrypoint" {
|
||||
description = "Traefik entryPoint name (e.g. 'web' or 'websecure')."
|
||||
type = string
|
||||
default = "web"
|
||||
}
|
||||
|
||||
# ─── Grafana Alloy ────────────────────────────────────────────────────────────
|
||||
|
||||
variable "alloy_image" {
|
||||
description = "Grafana Alloy container image."
|
||||
type = string
|
||||
default = "grafana/alloy:v1.5.0"
|
||||
}
|
||||
|
||||
variable "loki_endpoint" {
|
||||
description = "Loki push API URL."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "loki_auth_token" {
|
||||
description = "Bearer token for Loki. Leave empty for unauthenticated Loki."
|
||||
type = string
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
# ─── RabbitMQ ─────────────────────────────────────────────────────────────────
|
||||
|
||||
variable "enable_rabbitmq" {
|
||||
description = "Deploy RabbitMQ StatefulSet alongside the app."
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "rabbitmq_image" {
|
||||
description = "RabbitMQ Docker image."
|
||||
type = string
|
||||
default = "rabbitmq:3.13-management-alpine"
|
||||
}
|
||||
|
||||
variable "rabbitmq_user" {
|
||||
description = "RabbitMQ default user."
|
||||
type = string
|
||||
default = "guest"
|
||||
}
|
||||
|
||||
variable "rabbitmq_password" {
|
||||
description = "RabbitMQ default user password."
|
||||
type = string
|
||||
sensitive = true
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "rabbitmq_vhost" {
|
||||
description = "RabbitMQ default virtual host."
|
||||
type = string
|
||||
default = "/"
|
||||
}
|
||||
|
||||
variable "rabbitmq_pvc_size" {
|
||||
description = "Storage size for the RabbitMQ data PVC."
|
||||
type = string
|
||||
default = "2Gi"
|
||||
}
|
||||
|
||||
variable "rabbitmq_path_prefix" {
|
||||
description = "Traefik path prefix for the RabbitMQ management UI."
|
||||
type = string
|
||||
default = "/rabbitmq"
|
||||
}
|
||||
64
main.tf
Normal file
64
main.tf
Normal file
@@ -0,0 +1,64 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
docker = {
|
||||
source = "kreuzwerker/docker"
|
||||
version = "~> 3.9"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ─── Provider ─────────────────────────────────────────────────────────────────
|
||||
# Connects to the remote Docker daemon over SSH.
|
||||
# The SSH key path is supplied at runtime (typically via CI/CD from gopass).
|
||||
|
||||
provider "docker" {
|
||||
host = "ssh://${var.ssh_user}@${var.ssh_host}"
|
||||
ssh_opts = [
|
||||
"-i", var.ssh_key_path,
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "BatchMode=yes",
|
||||
]
|
||||
}
|
||||
|
||||
# ─── App Deployment ───────────────────────────────────────────────────────────
|
||||
# One module call = one app instance. The CI/CD pipeline runs tofu separately
|
||||
# per app using its own tfvars file and workspace.
|
||||
|
||||
module "app" {
|
||||
source = "./modules/app-openresty-pg-redis"
|
||||
|
||||
app_name = var.app_name
|
||||
environment = var.environment
|
||||
|
||||
openresty_source_type = var.openresty_source_type
|
||||
openresty_image = var.openresty_image
|
||||
openresty_external_port = var.openresty_external_port
|
||||
openresty_remote_config_path = var.openresty_remote_config_path
|
||||
openresty_local_build_context = var.openresty_local_build_context
|
||||
openresty_dockerfile = var.openresty_dockerfile
|
||||
openresty_git_repo = var.openresty_git_repo
|
||||
openresty_git_ref = var.openresty_git_ref
|
||||
openresty_git_token = var.openresty_git_token
|
||||
|
||||
db_name = var.db_name
|
||||
db_user = var.db_user
|
||||
db_password = var.db_password
|
||||
postgres_image = var.postgres_image
|
||||
redis_image = var.redis_image
|
||||
}
|
||||
|
||||
# ─── Outputs ──────────────────────────────────────────────────────────────────
|
||||
|
||||
output "openresty_url" {
|
||||
description = "URL to reach the deployed app."
|
||||
value = "http://${var.ssh_host}:${var.openresty_external_port}"
|
||||
}
|
||||
|
||||
output "app_containers" {
|
||||
description = "Names of all deployed containers."
|
||||
value = {
|
||||
openresty = module.app.openresty_container_name
|
||||
postgres = module.app.postgres_container_name
|
||||
redis = module.app.redis_container_name
|
||||
}
|
||||
}
|
||||
561
modules/app-k8s-nodered-rabbitmq/main.tf
Normal file
561
modules/app-k8s-nodered-rabbitmq/main.tf
Normal file
@@ -0,0 +1,561 @@
|
||||
locals {
|
||||
# Alloy config: bearer_token block only when a token is provided
|
||||
alloy_auth_line = var.loki_auth_token != "" ? " bearer_token = env(\"LOKI_AUTH_TOKEN\")\n" : ""
|
||||
|
||||
# Init container shell script:
|
||||
# - Copies all files from the image's data dir to the shared PVC mount (/data)
|
||||
# - Skips flows.json and flows_cred.json if they already exist on the PVC
|
||||
# (preserves user-modified flows across pod restarts / redeployments)
|
||||
# Shell variables ($SRC, $DEST etc.) are NOT HCL interpolations — only
|
||||
# ${var.init_data_src_path} is resolved by Tofu before the string reaches K8s.
|
||||
init_script = <<-EOT
|
||||
set -e
|
||||
SRC="${var.init_data_src_path}"
|
||||
DEST="/data_init"
|
||||
find "$SRC" -type f | while IFS= read -r src_file; do
|
||||
rel=$(echo "$src_file" | sed "s|^$SRC/||")
|
||||
dest_file="$DEST/$rel"
|
||||
if [ "$rel" = "flows.json" ] || [ "$rel" = "flows_cred.json" ]; then
|
||||
if [ -f "$dest_file" ]; then
|
||||
echo "Preserving existing $rel"
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
mkdir -p "$(dirname "$dest_file")"
|
||||
cp "$src_file" "$dest_file"
|
||||
echo "Copied $rel"
|
||||
done
|
||||
echo "Init complete."
|
||||
EOT
|
||||
}
|
||||
|
||||
# ─── Namespace ────────────────────────────────────────────────────────────────
|
||||
|
||||
resource "kubernetes_namespace" "app" {
|
||||
metadata {
|
||||
name = var.app_name
|
||||
labels = {
|
||||
"app.kubernetes.io/managed-by" = "opentofu"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ─── Secrets ──────────────────────────────────────────────────────────────────
|
||||
|
||||
resource "kubernetes_secret" "app" {
|
||||
metadata {
|
||||
name = "${var.app_name}-secret"
|
||||
namespace = kubernetes_namespace.app.metadata[0].name
|
||||
}
|
||||
|
||||
data = {
|
||||
LOKI_AUTH_TOKEN = var.loki_auth_token
|
||||
RABBITMQ_USER = var.rabbitmq_user
|
||||
RABBITMQ_PASSWORD = var.rabbitmq_password
|
||||
}
|
||||
}
|
||||
|
||||
# ─── Alloy ConfigMap ──────────────────────────────────────────────────────────
|
||||
# Generates the Grafana Alloy River config to tail /data/logs/*.log and push to Loki.
|
||||
|
||||
resource "kubernetes_config_map" "alloy" {
|
||||
metadata {
|
||||
name = "${var.app_name}-alloy-config"
|
||||
namespace = kubernetes_namespace.app.metadata[0].name
|
||||
}
|
||||
|
||||
data = {
|
||||
"config.alloy" = <<-EOT
|
||||
local.file_match "app_logs" {
|
||||
path_targets = [{
|
||||
__path__ = "/data/logs/*.log",
|
||||
app = "${var.app_name}",
|
||||
env = "${var.environment}",
|
||||
}]
|
||||
}
|
||||
|
||||
loki.source.file "app_logs" {
|
||||
targets = local.file_match.app_logs.targets
|
||||
forward_to = [loki.write.default.receiver]
|
||||
}
|
||||
|
||||
loki.write "default" {
|
||||
endpoint {
|
||||
url = "${var.loki_endpoint}"
|
||||
${local.alloy_auth_line} }
|
||||
external_labels = {
|
||||
app = "${var.app_name}",
|
||||
env = "${var.environment}",
|
||||
}
|
||||
}
|
||||
EOT
|
||||
}
|
||||
}
|
||||
|
||||
# ─── PVC: Alloy WAL ───────────────────────────────────────────────────────────
|
||||
# Small dedicated volume for the Grafana Alloy write-ahead log.
|
||||
# Storing the WAL here means Alloy remembers which log lines it has already sent
|
||||
# to Loki, preventing duplicate log entries after pod restarts.
|
||||
|
||||
resource "kubernetes_persistent_volume_claim" "alloy_wal" {
|
||||
metadata {
|
||||
name = "${var.app_name}-alloy-wal"
|
||||
namespace = kubernetes_namespace.app.metadata[0].name
|
||||
}
|
||||
|
||||
spec {
|
||||
access_modes = ["ReadWriteOnce"]
|
||||
|
||||
resources {
|
||||
requests = {
|
||||
storage = "100Mi"
|
||||
}
|
||||
}
|
||||
|
||||
storage_class_name = var.storage_class != "" ? var.storage_class : null
|
||||
}
|
||||
}
|
||||
|
||||
# ─── PVC: App Data ────────────────────────────────────────────────────────────
|
||||
# Persists Node-RED data directory across pod restarts.
|
||||
# The init container writes defaults here on first start; flows.json is never overwritten.
|
||||
|
||||
resource "kubernetes_persistent_volume_claim" "app_data" {
|
||||
metadata {
|
||||
name = "${var.app_name}-data"
|
||||
namespace = kubernetes_namespace.app.metadata[0].name
|
||||
}
|
||||
|
||||
spec {
|
||||
access_modes = ["ReadWriteOnce"]
|
||||
|
||||
resources {
|
||||
requests = {
|
||||
storage = var.app_pvc_size
|
||||
}
|
||||
}
|
||||
|
||||
storage_class_name = var.storage_class != "" ? var.storage_class : null
|
||||
}
|
||||
}
|
||||
|
||||
# ─── Deployment ───────────────────────────────────────────────────────────────
|
||||
|
||||
resource "kubernetes_deployment" "app" {
|
||||
metadata {
|
||||
name = var.app_name
|
||||
namespace = kubernetes_namespace.app.metadata[0].name
|
||||
labels = {
|
||||
"app.kubernetes.io/name" = var.app_name
|
||||
"app.kubernetes.io/managed-by" = "opentofu"
|
||||
}
|
||||
}
|
||||
|
||||
spec {
|
||||
replicas = 1
|
||||
|
||||
selector {
|
||||
match_labels = {
|
||||
app = var.app_name
|
||||
}
|
||||
}
|
||||
|
||||
template {
|
||||
metadata {
|
||||
labels = {
|
||||
app = var.app_name
|
||||
}
|
||||
}
|
||||
|
||||
spec {
|
||||
# ── Init container ──────────────────────────────────────────────────
|
||||
# Copies default configs and modules from the init image to the shared PVC.
|
||||
# flows.json and flows_cred.json are left untouched if they already exist.
|
||||
init_container {
|
||||
name = "${var.app_name}-init"
|
||||
image = var.init_container_image
|
||||
|
||||
command = ["/bin/sh", "-c", local.init_script]
|
||||
|
||||
volume_mount {
|
||||
name = "app-data"
|
||||
mount_path = "/data_init"
|
||||
}
|
||||
}
|
||||
|
||||
# ── App container ───────────────────────────────────────────────────
|
||||
container {
|
||||
name = "app"
|
||||
image = var.app_image
|
||||
|
||||
port {
|
||||
name = "http"
|
||||
container_port = var.app_port
|
||||
}
|
||||
|
||||
volume_mount {
|
||||
name = "app-data"
|
||||
mount_path = "/data"
|
||||
}
|
||||
|
||||
# RabbitMQ connection info (available even if RabbitMQ is disabled)
|
||||
env {
|
||||
name = "RABBITMQ_HOST"
|
||||
value = var.enable_rabbitmq ? "${var.app_name}-rabbitmq" : ""
|
||||
}
|
||||
env {
|
||||
name = "RABBITMQ_VHOST"
|
||||
value = var.rabbitmq_vhost
|
||||
}
|
||||
env {
|
||||
name = "RABBITMQ_USER"
|
||||
value_from {
|
||||
secret_key_ref {
|
||||
name = kubernetes_secret.app.metadata[0].name
|
||||
key = "RABBITMQ_USER"
|
||||
optional = true
|
||||
}
|
||||
}
|
||||
}
|
||||
env {
|
||||
name = "RABBITMQ_PASSWORD"
|
||||
value_from {
|
||||
secret_key_ref {
|
||||
name = kubernetes_secret.app.metadata[0].name
|
||||
key = "RABBITMQ_PASSWORD"
|
||||
optional = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ── Alloy sidecar ───────────────────────────────────────────────────
|
||||
# Tails /data/logs/*.log and forwards to Loki.
|
||||
# WAL is stored on its own PVC so log positions survive pod restarts —
|
||||
# no duplicate entries sent to Loki after a restart.
|
||||
container {
|
||||
name = "alloy"
|
||||
image = var.alloy_image
|
||||
|
||||
args = [
|
||||
"run",
|
||||
"--server.http.listen-addr=0.0.0.0:12345",
|
||||
"--storage.path=/var/lib/alloy/data",
|
||||
"/etc/alloy/config.alloy",
|
||||
]
|
||||
|
||||
port {
|
||||
name = "alloy-http"
|
||||
container_port = 12345
|
||||
}
|
||||
|
||||
# App data mounted read-only — Alloy only reads logs, never writes to /data
|
||||
volume_mount {
|
||||
name = "app-data"
|
||||
mount_path = "/data"
|
||||
read_only = true
|
||||
}
|
||||
|
||||
volume_mount {
|
||||
name = "alloy-config"
|
||||
mount_path = "/etc/alloy"
|
||||
read_only = true
|
||||
}
|
||||
|
||||
# Persistent WAL — survives pod restarts, prevents duplicate log shipping
|
||||
volume_mount {
|
||||
name = "alloy-wal"
|
||||
mount_path = "/var/lib/alloy/data"
|
||||
}
|
||||
|
||||
env {
|
||||
name = "LOKI_AUTH_TOKEN"
|
||||
value_from {
|
||||
secret_key_ref {
|
||||
name = kubernetes_secret.app.metadata[0].name
|
||||
key = "LOKI_AUTH_TOKEN"
|
||||
optional = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ── Volumes ─────────────────────────────────────────────────────────
|
||||
volume {
|
||||
name = "app-data"
|
||||
persistent_volume_claim {
|
||||
claim_name = kubernetes_persistent_volume_claim.app_data.metadata[0].name
|
||||
}
|
||||
}
|
||||
|
||||
volume {
|
||||
name = "alloy-config"
|
||||
config_map {
|
||||
name = kubernetes_config_map.alloy.metadata[0].name
|
||||
}
|
||||
}
|
||||
|
||||
volume {
|
||||
name = "alloy-wal"
|
||||
persistent_volume_claim {
|
||||
claim_name = kubernetes_persistent_volume_claim.alloy_wal.metadata[0].name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ─── Service: App ─────────────────────────────────────────────────────────────
|
||||
|
||||
resource "kubernetes_service" "app" {
|
||||
metadata {
|
||||
name = var.app_name
|
||||
namespace = kubernetes_namespace.app.metadata[0].name
|
||||
}
|
||||
|
||||
spec {
|
||||
selector = {
|
||||
app = var.app_name
|
||||
}
|
||||
|
||||
port {
|
||||
name = "http"
|
||||
port = var.app_port
|
||||
target_port = var.app_port
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ─── Traefik: App IngressRoute + StripPrefix Middleware ───────────────────────
|
||||
# Uses kubectl_manifest (gavinbunney/kubectl provider) instead of kubernetes_manifest.
|
||||
# kubectl_manifest does NOT validate CRD schemas at plan time, so tofu plan succeeds
|
||||
# even on a fresh cluster before Traefik CRDs are installed.
|
||||
# The API group is configurable via var.traefik_api_group to support both
|
||||
# k3s >= 1.27 (traefik.io/v1alpha1) and older clusters (traefik.containo.us/v1alpha1).
|
||||
|
||||
resource "kubectl_manifest" "app_middleware" {
|
||||
yaml_body = <<-YAML
|
||||
apiVersion: ${var.traefik_api_group}
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: ${var.app_name}-strip-prefix
|
||||
namespace: ${kubernetes_namespace.app.metadata[0].name}
|
||||
spec:
|
||||
stripPrefix:
|
||||
prefixes:
|
||||
- "${var.app_path_prefix}"
|
||||
YAML
|
||||
}
|
||||
|
||||
resource "kubectl_manifest" "app_ingress" {
|
||||
yaml_body = <<-YAML
|
||||
apiVersion: ${var.traefik_api_group}
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: ${var.app_name}
|
||||
namespace: ${kubernetes_namespace.app.metadata[0].name}
|
||||
spec:
|
||||
entryPoints:
|
||||
- ${var.traefik_entrypoint}
|
||||
routes:
|
||||
- match: "PathPrefix(`${var.app_path_prefix}`)"
|
||||
kind: Rule
|
||||
middlewares:
|
||||
- name: ${var.app_name}-strip-prefix
|
||||
namespace: ${kubernetes_namespace.app.metadata[0].name}
|
||||
services:
|
||||
- name: ${var.app_name}
|
||||
port: ${var.app_port}
|
||||
YAML
|
||||
|
||||
depends_on = [kubectl_manifest.app_middleware]
|
||||
}
|
||||
|
||||
# ─── RabbitMQ (optional) ──────────────────────────────────────────────────────
|
||||
|
||||
resource "kubernetes_stateful_set" "rabbitmq" {
|
||||
count = var.enable_rabbitmq ? 1 : 0
|
||||
|
||||
metadata {
|
||||
name = "${var.app_name}-rabbitmq"
|
||||
namespace = kubernetes_namespace.app.metadata[0].name
|
||||
labels = {
|
||||
"app.kubernetes.io/name" = "${var.app_name}-rabbitmq"
|
||||
"app.kubernetes.io/managed-by" = "opentofu"
|
||||
}
|
||||
}
|
||||
|
||||
spec {
|
||||
service_name = "${var.app_name}-rabbitmq-headless"
|
||||
replicas = 1
|
||||
|
||||
selector {
|
||||
match_labels = {
|
||||
app = "${var.app_name}-rabbitmq"
|
||||
}
|
||||
}
|
||||
|
||||
template {
|
||||
metadata {
|
||||
labels = {
|
||||
app = "${var.app_name}-rabbitmq"
|
||||
}
|
||||
}
|
||||
|
||||
spec {
|
||||
container {
|
||||
name = "rabbitmq"
|
||||
image = var.rabbitmq_image
|
||||
|
||||
port {
|
||||
name = "amqp"
|
||||
container_port = 5672
|
||||
}
|
||||
port {
|
||||
name = "management"
|
||||
container_port = 15672
|
||||
}
|
||||
|
||||
env {
|
||||
name = "RABBITMQ_DEFAULT_USER"
|
||||
value_from {
|
||||
secret_key_ref {
|
||||
name = kubernetes_secret.app.metadata[0].name
|
||||
key = "RABBITMQ_USER"
|
||||
}
|
||||
}
|
||||
}
|
||||
env {
|
||||
name = "RABBITMQ_DEFAULT_PASS"
|
||||
value_from {
|
||||
secret_key_ref {
|
||||
name = kubernetes_secret.app.metadata[0].name
|
||||
key = "RABBITMQ_PASSWORD"
|
||||
}
|
||||
}
|
||||
}
|
||||
env {
|
||||
name = "RABBITMQ_DEFAULT_VHOST"
|
||||
value = var.rabbitmq_vhost
|
||||
}
|
||||
|
||||
volume_mount {
|
||||
name = "rabbitmq-data"
|
||||
mount_path = "/var/lib/rabbitmq"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
volume_claim_template {
|
||||
metadata {
|
||||
name = "rabbitmq-data"
|
||||
}
|
||||
spec {
|
||||
access_modes = ["ReadWriteOnce"]
|
||||
resources {
|
||||
requests = {
|
||||
storage = var.rabbitmq_pvc_size
|
||||
}
|
||||
}
|
||||
storage_class_name = var.storage_class != "" ? var.storage_class : null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Headless service — required by the StatefulSet for stable pod DNS names
|
||||
resource "kubernetes_service" "rabbitmq_headless" {
|
||||
count = var.enable_rabbitmq ? 1 : 0
|
||||
|
||||
metadata {
|
||||
name = "${var.app_name}-rabbitmq-headless"
|
||||
namespace = kubernetes_namespace.app.metadata[0].name
|
||||
}
|
||||
|
||||
spec {
|
||||
cluster_ip = "None"
|
||||
selector = {
|
||||
app = "${var.app_name}-rabbitmq"
|
||||
}
|
||||
port {
|
||||
name = "amqp"
|
||||
port = 5672
|
||||
}
|
||||
port {
|
||||
name = "management"
|
||||
port = 15672
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ClusterIP service — used by the app container and the management IngressRoute
|
||||
resource "kubernetes_service" "rabbitmq" {
|
||||
count = var.enable_rabbitmq ? 1 : 0
|
||||
|
||||
metadata {
|
||||
name = "${var.app_name}-rabbitmq"
|
||||
namespace = kubernetes_namespace.app.metadata[0].name
|
||||
}
|
||||
|
||||
spec {
|
||||
selector = {
|
||||
app = "${var.app_name}-rabbitmq"
|
||||
}
|
||||
port {
|
||||
name = "amqp"
|
||||
port = 5672
|
||||
target_port = 5672
|
||||
}
|
||||
port {
|
||||
name = "management"
|
||||
port = 15672
|
||||
target_port = 15672
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Traefik Middleware: StripPrefix for RabbitMQ management UI
|
||||
resource "kubectl_manifest" "rabbitmq_middleware" {
|
||||
count = var.enable_rabbitmq ? 1 : 0
|
||||
|
||||
yaml_body = <<-YAML
|
||||
apiVersion: ${var.traefik_api_group}
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: ${var.app_name}-rabbitmq-strip-prefix
|
||||
namespace: ${kubernetes_namespace.app.metadata[0].name}
|
||||
spec:
|
||||
stripPrefix:
|
||||
prefixes:
|
||||
- "${var.rabbitmq_path_prefix}"
|
||||
YAML
|
||||
}
|
||||
|
||||
# Traefik IngressRoute for RabbitMQ management UI (port 15672)
|
||||
resource "kubectl_manifest" "rabbitmq_ingress" {
|
||||
count = var.enable_rabbitmq ? 1 : 0
|
||||
|
||||
yaml_body = <<-YAML
|
||||
apiVersion: ${var.traefik_api_group}
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: ${var.app_name}-rabbitmq
|
||||
namespace: ${kubernetes_namespace.app.metadata[0].name}
|
||||
spec:
|
||||
entryPoints:
|
||||
- ${var.traefik_entrypoint}
|
||||
routes:
|
||||
- match: "PathPrefix(`${var.rabbitmq_path_prefix}`)"
|
||||
kind: Rule
|
||||
middlewares:
|
||||
- name: ${var.app_name}-rabbitmq-strip-prefix
|
||||
namespace: ${kubernetes_namespace.app.metadata[0].name}
|
||||
services:
|
||||
- name: ${var.app_name}-rabbitmq
|
||||
port: 15672
|
||||
YAML
|
||||
|
||||
depends_on = [kubectl_manifest.rabbitmq_middleware]
|
||||
}
|
||||
39
modules/app-k8s-nodered-rabbitmq/outputs.tf
Normal file
39
modules/app-k8s-nodered-rabbitmq/outputs.tf
Normal file
@@ -0,0 +1,39 @@
|
||||
output "namespace" {
|
||||
description = "Kubernetes namespace created for this app."
|
||||
value = kubernetes_namespace.app.metadata[0].name
|
||||
}
|
||||
|
||||
output "app_service_name" {
|
||||
description = "Kubernetes Service name for the app (use for in-cluster DNS: <name>.<namespace>.svc)."
|
||||
value = kubernetes_service.app.metadata[0].name
|
||||
}
|
||||
|
||||
output "app_path_prefix" {
|
||||
description = "Traefik path prefix the app is reachable at."
|
||||
value = var.app_path_prefix
|
||||
}
|
||||
|
||||
output "app_pvc_name" {
|
||||
description = "Name of the PVC holding the app data directory."
|
||||
value = kubernetes_persistent_volume_claim.app_data.metadata[0].name
|
||||
}
|
||||
|
||||
output "alloy_config_map" {
|
||||
description = "Name of the Alloy ConfigMap."
|
||||
value = kubernetes_config_map.alloy.metadata[0].name
|
||||
}
|
||||
|
||||
output "rabbitmq_service_name" {
|
||||
description = "Kubernetes Service name for RabbitMQ. Empty string if RabbitMQ is disabled."
|
||||
value = var.enable_rabbitmq ? kubernetes_service.rabbitmq[0].metadata[0].name : ""
|
||||
}
|
||||
|
||||
output "rabbitmq_amqp_url" {
|
||||
description = "AMQP URL for in-cluster app connections. Empty string if RabbitMQ is disabled."
|
||||
value = var.enable_rabbitmq ? "amqp://${var.app_name}-rabbitmq.${kubernetes_namespace.app.metadata[0].name}.svc:5672/${var.rabbitmq_vhost}" : ""
|
||||
}
|
||||
|
||||
output "rabbitmq_path_prefix" {
|
||||
description = "Traefik path prefix for the RabbitMQ management UI. Empty string if disabled."
|
||||
value = var.enable_rabbitmq ? var.rabbitmq_path_prefix : ""
|
||||
}
|
||||
146
modules/app-k8s-nodered-rabbitmq/variables.tf
Normal file
146
modules/app-k8s-nodered-rabbitmq/variables.tf
Normal file
@@ -0,0 +1,146 @@
|
||||
# ─── App Identity ─────────────────────────────────────────────────────────────
|
||||
|
||||
variable "app_name" {
|
||||
description = "Unique name for this app. Used as K8s namespace, deployment name, and resource prefix."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "environment" {
|
||||
description = "Deployment environment label (attached to Loki log streams as an external label)."
|
||||
type = string
|
||||
default = "dev"
|
||||
}
|
||||
|
||||
# ─── App Container ────────────────────────────────────────────────────────────
|
||||
|
||||
variable "app_image" {
|
||||
description = "Container image for the main application (e.g. nodered/node-red:3.1.9)."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "app_port" {
|
||||
description = "Port the app container listens on internally."
|
||||
type = number
|
||||
default = 1880
|
||||
}
|
||||
|
||||
# ─── Init Container ───────────────────────────────────────────────────────────
|
||||
|
||||
variable "init_container_image" {
|
||||
description = <<-EOT
|
||||
Image for the init container. Must contain default config and module files
|
||||
at init_data_src_path. The init container copies those files to the shared
|
||||
PVC, but preserves flows.json and flows_cred.json if they already exist.
|
||||
EOT
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "init_data_src_path" {
|
||||
description = "Absolute path inside the init container image where default data files live."
|
||||
type = string
|
||||
default = "/app-data"
|
||||
}
|
||||
|
||||
# ─── Storage ──────────────────────────────────────────────────────────────────
|
||||
|
||||
variable "storage_class" {
|
||||
description = "Kubernetes StorageClass for PVCs. Empty string uses the cluster default (k3s: local-path)."
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "app_pvc_size" {
|
||||
description = "Storage size for the app data PVC (e.g. '2Gi')."
|
||||
type = string
|
||||
default = "2Gi"
|
||||
}
|
||||
|
||||
# ─── Ingress (Traefik CRDs) ───────────────────────────────────────────────────
|
||||
|
||||
variable "app_path_prefix" {
|
||||
description = "URL path prefix for the app IngressRoute (e.g. '/myapp'). Prefix is stripped before forwarding."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "traefik_entrypoint" {
|
||||
description = "Traefik entryPoint name to attach IngressRoutes to (e.g. 'web' or 'websecure')."
|
||||
type = string
|
||||
default = "web"
|
||||
}
|
||||
|
||||
# ─── Grafana Alloy Sidecar ────────────────────────────────────────────────────
|
||||
|
||||
variable "alloy_image" {
|
||||
description = "Grafana Alloy container image."
|
||||
type = string
|
||||
default = "grafana/alloy:v1.5.0"
|
||||
}
|
||||
|
||||
variable "loki_endpoint" {
|
||||
description = "Loki push API URL (e.g. 'http://loki.monitoring.svc:3100/loki/api/v1/push')."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "loki_auth_token" {
|
||||
description = "Bearer token for Loki authentication. Leave empty for unauthenticated Loki."
|
||||
type = string
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
# ─── RabbitMQ (optional) ──────────────────────────────────────────────────────
|
||||
|
||||
variable "enable_rabbitmq" {
|
||||
description = "Deploy a RabbitMQ StatefulSet alongside the app."
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "rabbitmq_image" {
|
||||
description = "RabbitMQ Docker image. Use the -management variant to enable the management UI."
|
||||
type = string
|
||||
default = "rabbitmq:3.13-management-alpine"
|
||||
}
|
||||
|
||||
variable "rabbitmq_user" {
|
||||
description = "RabbitMQ default user."
|
||||
type = string
|
||||
default = "guest"
|
||||
}
|
||||
|
||||
variable "rabbitmq_password" {
|
||||
description = "RabbitMQ default user password."
|
||||
type = string
|
||||
sensitive = true
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "rabbitmq_vhost" {
|
||||
description = "RabbitMQ default virtual host."
|
||||
type = string
|
||||
default = "/"
|
||||
}
|
||||
|
||||
variable "rabbitmq_pvc_size" {
|
||||
description = "Storage size for the RabbitMQ data PVC."
|
||||
type = string
|
||||
default = "2Gi"
|
||||
}
|
||||
|
||||
variable "rabbitmq_path_prefix" {
|
||||
description = "URL path prefix for the RabbitMQ management UI IngressRoute."
|
||||
type = string
|
||||
default = "/rabbitmq"
|
||||
}
|
||||
|
||||
# ─── Traefik CRD API group ────────────────────────────────────────────────────
|
||||
|
||||
variable "traefik_api_group" {
|
||||
description = <<-EOT
|
||||
Traefik CRD API group and version used for IngressRoute and Middleware resources.
|
||||
k3s >= 1.27 (Traefik v2.9+): "traefik.io/v1alpha1"
|
||||
k3s < 1.27 (Traefik v2.x): "traefik.containo.us/v1alpha1"
|
||||
EOT
|
||||
type = string
|
||||
default = "traefik.io/v1alpha1"
|
||||
}
|
||||
180
modules/app-openresty-pg-redis/main.tf
Normal file
180
modules/app-openresty-pg-redis/main.tf
Normal file
@@ -0,0 +1,180 @@
|
||||
locals {
|
||||
persistent = var.environment == "prod"
|
||||
|
||||
# Inject oauth2 token into git URL for private repos
|
||||
git_repo_url = (
|
||||
var.openresty_git_token != ""
|
||||
? replace(var.openresty_git_repo, "://", "://oauth2:${var.openresty_git_token}@")
|
||||
: var.openresty_git_repo
|
||||
)
|
||||
|
||||
# Entrypoint for git_clone mode: installs git via apk, clones the pinned ref,
|
||||
# then hands off to openresty. The cloned repo must have an 'openresty/' subdirectory
|
||||
# containing a valid nginx.conf (used as the -p prefix path).
|
||||
git_clone_entrypoint = [
|
||||
"/bin/sh", "-c",
|
||||
"apk add --no-cache git && git clone --depth 1 --branch '${var.openresty_git_ref}' '${local.git_repo_url}' /tmp/app && exec openresty -g 'daemon off;' -p /tmp/app/openresty"
|
||||
]
|
||||
}
|
||||
|
||||
# ─── Network ──────────────────────────────────────────────────────────────────
|
||||
# Each app gets its own isolated bridge network. Postgres and Redis are not
|
||||
# exposed externally; only OpenResty has a published port.
|
||||
|
||||
resource "docker_network" "app" {
|
||||
name = "${var.app_name}-network"
|
||||
driver = "bridge"
|
||||
}
|
||||
|
||||
# ─── Volumes (prod only) ──────────────────────────────────────────────────────
|
||||
# In dev mode containers are ephemeral; volumes are created only for prod.
|
||||
|
||||
resource "docker_volume" "postgres" {
|
||||
count = local.persistent ? 1 : 0
|
||||
name = "${var.app_name}-postgres-data"
|
||||
}
|
||||
|
||||
resource "docker_volume" "redis" {
|
||||
count = local.persistent ? 1 : 0
|
||||
name = "${var.app_name}-redis-data"
|
||||
}
|
||||
|
||||
# ─── Images ───────────────────────────────────────────────────────────────────
|
||||
|
||||
# Standard OpenResty image — used for bind_mount and git_clone modes.
|
||||
resource "docker_image" "openresty" {
|
||||
count = var.openresty_source_type != "local_build" ? 1 : 0
|
||||
name = var.openresty_image
|
||||
keep_locally = true
|
||||
}
|
||||
|
||||
# Custom-built OpenResty image — used for local_build mode.
|
||||
# The build context is transferred from the local machine to the remote Docker
|
||||
# daemon over SSH and built there. Rebuilds are triggered by changes to any
|
||||
# file in the context directory.
|
||||
resource "docker_image" "openresty_custom" {
|
||||
count = var.openresty_source_type == "local_build" ? 1 : 0
|
||||
name = "${var.app_name}-openresty:latest"
|
||||
keep_locally = true
|
||||
|
||||
build {
|
||||
context = var.openresty_local_build_context
|
||||
dockerfile = var.openresty_dockerfile
|
||||
}
|
||||
|
||||
triggers = {
|
||||
# Rebuild when any file in the build context changes.
|
||||
context_hash = sha1(join("", [
|
||||
for f in sort(fileset(var.openresty_local_build_context, "**/*")) :
|
||||
filesha1("${var.openresty_local_build_context}/${f}")
|
||||
]))
|
||||
}
|
||||
}
|
||||
|
||||
resource "docker_image" "postgres" {
|
||||
name = var.postgres_image
|
||||
keep_locally = true
|
||||
}
|
||||
|
||||
resource "docker_image" "redis" {
|
||||
name = var.redis_image
|
||||
keep_locally = true
|
||||
}
|
||||
|
||||
# ─── PostgreSQL ───────────────────────────────────────────────────────────────
|
||||
|
||||
resource "docker_container" "postgres" {
|
||||
image = docker_image.postgres.image_id
|
||||
name = "${var.app_name}-postgres"
|
||||
restart = "unless-stopped"
|
||||
|
||||
networks_advanced {
|
||||
name = docker_network.app.name
|
||||
}
|
||||
|
||||
env = [
|
||||
"POSTGRES_DB=${var.db_name}",
|
||||
"POSTGRES_USER=${var.db_user}",
|
||||
"POSTGRES_PASSWORD=${var.db_password}",
|
||||
]
|
||||
|
||||
dynamic "volumes" {
|
||||
for_each = local.persistent ? [1] : []
|
||||
content {
|
||||
volume_name = docker_volume.postgres[0].name
|
||||
container_path = "/var/lib/postgresql/data"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ─── Redis ────────────────────────────────────────────────────────────────────
|
||||
|
||||
resource "docker_container" "redis" {
|
||||
image = docker_image.redis.image_id
|
||||
name = "${var.app_name}-redis"
|
||||
restart = "unless-stopped"
|
||||
|
||||
networks_advanced {
|
||||
name = docker_network.app.name
|
||||
}
|
||||
|
||||
dynamic "volumes" {
|
||||
for_each = local.persistent ? [1] : []
|
||||
content {
|
||||
volume_name = docker_volume.redis[0].name
|
||||
container_path = "/data"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ─── OpenResty ────────────────────────────────────────────────────────────────
|
||||
|
||||
resource "docker_container" "openresty" {
|
||||
image = (
|
||||
var.openresty_source_type == "local_build"
|
||||
? docker_image.openresty_custom[0].image_id
|
||||
: docker_image.openresty[0].image_id
|
||||
)
|
||||
name = "${var.app_name}-openresty"
|
||||
restart = "unless-stopped"
|
||||
|
||||
networks_advanced {
|
||||
name = docker_network.app.name
|
||||
}
|
||||
|
||||
ports {
|
||||
internal = 80
|
||||
external = var.openresty_external_port
|
||||
}
|
||||
|
||||
# bind_mount: mount a pre-existing directory from the remote host.
|
||||
# The directory must contain a valid nginx.conf and any Lua files.
|
||||
dynamic "volumes" {
|
||||
for_each = var.openresty_source_type == "bind_mount" ? [1] : []
|
||||
content {
|
||||
host_path = var.openresty_remote_config_path
|
||||
container_path = "/usr/local/openresty/nginx/conf"
|
||||
read_only = true
|
||||
}
|
||||
}
|
||||
|
||||
# git_clone: override the container entrypoint to clone the repo and start openresty.
|
||||
# The base image must support apk (Alpine). The cloned repo must have an openresty/
|
||||
# subdirectory with nginx.conf.
|
||||
entrypoint = var.openresty_source_type == "git_clone" ? local.git_clone_entrypoint : null
|
||||
|
||||
# Expose service connection info as env vars so Lua code can use them via os.getenv().
|
||||
env = [
|
||||
"APP_NAME=${var.app_name}",
|
||||
"POSTGRES_HOST=${var.app_name}-postgres",
|
||||
"POSTGRES_DB=${var.db_name}",
|
||||
"POSTGRES_USER=${var.db_user}",
|
||||
"POSTGRES_PASSWORD=${var.db_password}",
|
||||
"REDIS_HOST=${var.app_name}-redis",
|
||||
]
|
||||
|
||||
depends_on = [
|
||||
docker_container.postgres,
|
||||
docker_container.redis,
|
||||
]
|
||||
}
|
||||
34
modules/app-openresty-pg-redis/outputs.tf
Normal file
34
modules/app-openresty-pg-redis/outputs.tf
Normal file
@@ -0,0 +1,34 @@
|
||||
output "openresty_container_name" {
|
||||
description = "Name of the OpenResty container on the remote host."
|
||||
value = docker_container.openresty.name
|
||||
}
|
||||
|
||||
output "openresty_external_port" {
|
||||
description = "External port OpenResty is reachable on."
|
||||
value = var.openresty_external_port
|
||||
}
|
||||
|
||||
output "postgres_container_name" {
|
||||
description = "Name of the PostgreSQL container (reachable within the app network)."
|
||||
value = docker_container.postgres.name
|
||||
}
|
||||
|
||||
output "redis_container_name" {
|
||||
description = "Name of the Redis container (reachable within the app network)."
|
||||
value = docker_container.redis.name
|
||||
}
|
||||
|
||||
output "network_name" {
|
||||
description = "Name of the Docker bridge network shared by all app containers."
|
||||
value = docker_network.app.name
|
||||
}
|
||||
|
||||
output "postgres_volume_name" {
|
||||
description = "Name of the PostgreSQL data volume. Empty string in dev (ephemeral) mode."
|
||||
value = local.persistent ? docker_volume.postgres[0].name : ""
|
||||
}
|
||||
|
||||
output "redis_volume_name" {
|
||||
description = "Name of the Redis data volume. Empty string in dev (ephemeral) mode."
|
||||
value = local.persistent ? docker_volume.redis[0].name : ""
|
||||
}
|
||||
152
modules/app-openresty-pg-redis/variables.tf
Normal file
152
modules/app-openresty-pg-redis/variables.tf
Normal file
@@ -0,0 +1,152 @@
|
||||
# ─── App Identity ─────────────────────────────────────────────────────────────
|
||||
|
||||
variable "app_name" {
|
||||
description = "Unique name for this app deployment. Used as prefix for all container, network and volume names."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "environment" {
|
||||
description = "Deployment environment. Controls volume persistence: 'prod' = named volumes, 'dev' = ephemeral."
|
||||
type = string
|
||||
default = "dev"
|
||||
|
||||
validation {
|
||||
condition = contains(["prod", "dev"], var.environment)
|
||||
error_message = "environment must be 'prod' or 'dev'."
|
||||
}
|
||||
}
|
||||
|
||||
# ─── OpenResty: source type ────────────────────────────────────────────────────
|
||||
|
||||
variable "openresty_source_type" {
|
||||
description = <<-EOT
|
||||
How to provide OpenResty config / Lua code. One of:
|
||||
bind_mount - mount an existing directory from the remote host filesystem
|
||||
local_build - build a Docker image from a local Dockerfile context (sent to remote daemon)
|
||||
git_clone - clone a git repo at container startup (requires git-capable base image or apk)
|
||||
EOT
|
||||
type = string
|
||||
|
||||
validation {
|
||||
condition = contains(["bind_mount", "local_build", "git_clone"], var.openresty_source_type)
|
||||
error_message = "openresty_source_type must be 'bind_mount', 'local_build', or 'git_clone'."
|
||||
}
|
||||
}
|
||||
|
||||
# ─── OpenResty: base image (bind_mount / git_clone) ───────────────────────────
|
||||
|
||||
variable "openresty_image" {
|
||||
description = "OpenResty Docker image used for bind_mount and git_clone modes."
|
||||
type = string
|
||||
default = "openresty/openresty:1.25.3-alpine"
|
||||
}
|
||||
|
||||
# ─── OpenResty: bind_mount options ────────────────────────────────────────────
|
||||
|
||||
variable "openresty_remote_config_path" {
|
||||
description = <<-EOT
|
||||
Absolute path on the REMOTE HOST to mount as /usr/local/openresty/nginx/conf inside the container.
|
||||
Only used when openresty_source_type = 'bind_mount'.
|
||||
EOT
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
# ─── OpenResty: local_build options ───────────────────────────────────────────
|
||||
|
||||
variable "openresty_local_build_context" {
|
||||
description = <<-EOT
|
||||
Path to the local Dockerfile build context directory (relative to the tofu working directory).
|
||||
The context is transferred to the remote Docker daemon over SSH and built there.
|
||||
Only used when openresty_source_type = 'local_build'.
|
||||
EOT
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "openresty_dockerfile" {
|
||||
description = "Dockerfile filename inside the build context. Only used when openresty_source_type = 'local_build'."
|
||||
type = string
|
||||
default = "Dockerfile"
|
||||
}
|
||||
|
||||
# ─── OpenResty: git_clone options ─────────────────────────────────────────────
|
||||
|
||||
variable "openresty_git_repo" {
|
||||
description = <<-EOT
|
||||
Git repository URL to clone at container startup.
|
||||
Only used when openresty_source_type = 'git_clone'.
|
||||
The cloned repo must contain an 'openresty/' directory with a valid nginx.conf.
|
||||
EOT
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "openresty_git_ref" {
|
||||
description = <<-EOT
|
||||
Git ref to checkout. Must be a pinned tag or full commit SHA — never a branch name.
|
||||
Mutable branch names cause non-reproducible container restarts (the same image
|
||||
could contain different code each time the container is recreated).
|
||||
Only used when openresty_source_type = 'git_clone'.
|
||||
EOT
|
||||
type = string
|
||||
default = ""
|
||||
|
||||
validation {
|
||||
condition = var.openresty_git_ref == "" || !contains(
|
||||
["main", "master", "develop", "dev", "staging", "HEAD", "latest", "trunk"],
|
||||
var.openresty_git_ref
|
||||
)
|
||||
error_message = "openresty_git_ref must be a pinned tag or commit SHA, not a mutable branch name. Got: '${var.openresty_git_ref}'."
|
||||
}
|
||||
}
|
||||
|
||||
variable "openresty_git_token" {
|
||||
description = <<-EOT
|
||||
Optional personal access token for private git repositories.
|
||||
Injected into the clone URL as oauth2:<token>@.
|
||||
Only used when openresty_source_type = 'git_clone'.
|
||||
EOT
|
||||
type = string
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
# ─── Networking & Ports ───────────────────────────────────────────────────────
|
||||
|
||||
variable "openresty_external_port" {
|
||||
description = "Port exposed on the remote host that forwards to OpenResty port 80."
|
||||
type = number
|
||||
}
|
||||
|
||||
# ─── PostgreSQL ───────────────────────────────────────────────────────────────
|
||||
|
||||
variable "db_name" {
|
||||
description = "PostgreSQL database name."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "db_user" {
|
||||
description = "PostgreSQL user."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "db_password" {
|
||||
description = "PostgreSQL password."
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "postgres_image" {
|
||||
description = "PostgreSQL Docker image."
|
||||
type = string
|
||||
default = "postgres:16-alpine"
|
||||
}
|
||||
|
||||
# ─── Redis ────────────────────────────────────────────────────────────────────
|
||||
|
||||
variable "redis_image" {
|
||||
description = "Redis Docker image."
|
||||
type = string
|
||||
default = "redis:7-alpine"
|
||||
}
|
||||
85
scripts/setup-backend.sh
Normal file
85
scripts/setup-backend.sh
Normal file
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env bash
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# setup-backend.sh — One-time SeaweedFS state backend initialisation
|
||||
#
|
||||
# Run this once per machine (or CI runner) before using the pipelines.
|
||||
# It uncommets the backend block in both stack backend.tf files and runs
|
||||
# tofu init with the correct -backend-config flags.
|
||||
#
|
||||
# Usage:
|
||||
# chmod +x scripts/setup-backend.sh
|
||||
# ./scripts/setup-backend.sh
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
# ── Collect config ─────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "SeaweedFS S3 State Backend Setup"
|
||||
echo "═════════════════════════════════"
|
||||
echo ""
|
||||
|
||||
read -rp "SeaweedFS S3 endpoint (e.g. http://seaweedfs.example.com:8333): " ENDPOINT
|
||||
read -rp "Access key: " ACCESS_KEY
|
||||
read -rsp "Secret key: " SECRET_KEY
|
||||
echo ""
|
||||
read -rp "State bucket name [tofu-state]: " BUCKET
|
||||
BUCKET="${BUCKET:-tofu-state}"
|
||||
|
||||
export AWS_ACCESS_KEY_ID="$ACCESS_KEY"
|
||||
export AWS_SECRET_ACCESS_KEY="$SECRET_KEY"
|
||||
|
||||
BACKEND_ARGS=(
|
||||
"-backend-config=bucket=${BUCKET}"
|
||||
"-backend-config=endpoint=${ENDPOINT}"
|
||||
"-backend-config=region=us-east-1"
|
||||
"-backend-config=force_path_style=true"
|
||||
)
|
||||
|
||||
# ── Helper: enable backend block in a backend.tf ──────────────────────────
|
||||
enable_backend() {
|
||||
local file="$1"
|
||||
if grep -q '# terraform {' "$file"; then
|
||||
sed -i \
|
||||
-e 's|^# terraform {|terraform {|' \
|
||||
-e 's|^# backend "s3" {}| backend "s3" {}|' \
|
||||
-e 's|^# }$|}|' \
|
||||
"$file"
|
||||
echo " Enabled S3 backend block in $file"
|
||||
else
|
||||
echo " Backend block already enabled in $file"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Docker stack ──────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "── Docker stack ──────────────────────────────────────────────────"
|
||||
enable_backend "$REPO_ROOT/backend.tf"
|
||||
|
||||
cd "$REPO_ROOT"
|
||||
echo " Running: tofu init (Docker stack)"
|
||||
tofu init "${BACKEND_ARGS[@]}" "-backend-config=key=apps/PLACEHOLDER.tfstate" -reconfigure
|
||||
echo " Docker stack backend initialised."
|
||||
|
||||
# ── Kubernetes stack ──────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "── Kubernetes stack ──────────────────────────────────────────────"
|
||||
enable_backend "$REPO_ROOT/k8s/backend.tf"
|
||||
|
||||
cd "$REPO_ROOT/k8s"
|
||||
echo " Running: tofu init (K8s stack)"
|
||||
tofu init "${BACKEND_ARGS[@]}" "-backend-config=key=apps-k8s/PLACEHOLDER.tfstate" -reconfigure
|
||||
echo " Kubernetes stack backend initialised."
|
||||
|
||||
# ── Done ──────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "Done. Both stacks are now configured to use SeaweedFS for state storage."
|
||||
echo ""
|
||||
echo "Next: commit the updated backend.tf files, then add these as Gitea secrets:"
|
||||
echo " SEAWEED_S3_ENDPOINT = ${ENDPOINT}"
|
||||
echo " SEAWEED_ACCESS_KEY = ${ACCESS_KEY}"
|
||||
echo " SEAWEED_SECRET_KEY = (not shown)"
|
||||
echo " SEAWEED_BUCKET = ${BUCKET}"
|
||||
echo ""
|
||||
echo "Do NOT commit the access/secret keys. They go in Gitea secrets only."
|
||||
146
tests/docker_validation.tftest.hcl
Normal file
146
tests/docker_validation.tftest.hcl
Normal file
@@ -0,0 +1,146 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Docker stack unit tests — run with: tofu test (from repo root)
|
||||
# Requires OpenTofu >= 1.7.0 (mock_provider support)
|
||||
#
|
||||
# All providers are mocked — no real Docker host needed.
|
||||
# Tests cover: variable validation, plan structure, source type modes.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
mock_provider "docker" {}
|
||||
|
||||
# ─── Shared baseline variables ────────────────────────────────────────────────
|
||||
# Every run block below can override individual values.
|
||||
|
||||
variables {
|
||||
ssh_host = "test-host.example.com"
|
||||
ssh_user = "deploy"
|
||||
ssh_key_path = "/tmp/test-key"
|
||||
|
||||
app_name = "testapp"
|
||||
environment = "dev"
|
||||
|
||||
openresty_source_type = "bind_mount"
|
||||
openresty_remote_config_path = "/opt/apps/testapp/openresty"
|
||||
openresty_external_port = 8080
|
||||
|
||||
db_name = "testdb"
|
||||
db_user = "testuser"
|
||||
db_password = "test-secret"
|
||||
}
|
||||
|
||||
# ─── Smoke tests: valid configurations plan without error ─────────────────────
|
||||
|
||||
run "valid_dev_bind_mount" {
|
||||
command = plan
|
||||
# Verifies the baseline dev/bind_mount config produces a valid plan.
|
||||
}
|
||||
|
||||
run "valid_prod_bind_mount" {
|
||||
command = plan
|
||||
variables {
|
||||
environment = "prod"
|
||||
}
|
||||
}
|
||||
|
||||
run "valid_git_clone_with_tag" {
|
||||
command = plan
|
||||
variables {
|
||||
openresty_source_type = "git_clone"
|
||||
openresty_git_repo = "https://gitea.example.com/org/myapp.git"
|
||||
openresty_git_ref = "v2.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
run "valid_git_clone_with_commit_sha" {
|
||||
command = plan
|
||||
variables {
|
||||
openresty_source_type = "git_clone"
|
||||
openresty_git_repo = "https://gitea.example.com/org/myapp.git"
|
||||
openresty_git_ref = "a3f8c1d2e4b56789abcdef0123456789abcdef01"
|
||||
}
|
||||
}
|
||||
|
||||
run "valid_local_build" {
|
||||
command = plan
|
||||
variables {
|
||||
openresty_source_type = "local_build"
|
||||
openresty_local_build_context = "./openresty"
|
||||
openresty_dockerfile = "Dockerfile"
|
||||
}
|
||||
}
|
||||
|
||||
# ─── Validation: git_ref must not be a mutable branch name ───────────────────
|
||||
|
||||
run "reject_git_ref_main" {
|
||||
command = plan
|
||||
variables {
|
||||
openresty_source_type = "git_clone"
|
||||
openresty_git_repo = "https://gitea.example.com/org/myapp.git"
|
||||
openresty_git_ref = "main"
|
||||
}
|
||||
expect_failures = [var.openresty_git_ref]
|
||||
}
|
||||
|
||||
run "reject_git_ref_master" {
|
||||
command = plan
|
||||
variables {
|
||||
openresty_source_type = "git_clone"
|
||||
openresty_git_ref = "master"
|
||||
}
|
||||
expect_failures = [var.openresty_git_ref]
|
||||
}
|
||||
|
||||
run "reject_git_ref_develop" {
|
||||
command = plan
|
||||
variables {
|
||||
openresty_source_type = "git_clone"
|
||||
openresty_git_ref = "develop"
|
||||
}
|
||||
expect_failures = [var.openresty_git_ref]
|
||||
}
|
||||
|
||||
run "reject_git_ref_HEAD" {
|
||||
command = plan
|
||||
variables {
|
||||
openresty_source_type = "git_clone"
|
||||
openresty_git_ref = "HEAD"
|
||||
}
|
||||
expect_failures = [var.openresty_git_ref]
|
||||
}
|
||||
|
||||
run "reject_git_ref_latest" {
|
||||
command = plan
|
||||
variables {
|
||||
openresty_source_type = "git_clone"
|
||||
openresty_git_ref = "latest"
|
||||
}
|
||||
expect_failures = [var.openresty_git_ref]
|
||||
}
|
||||
|
||||
# ─── Validation: environment must be 'prod' or 'dev' ─────────────────────────
|
||||
|
||||
run "reject_environment_staging" {
|
||||
command = plan
|
||||
variables {
|
||||
environment = "staging"
|
||||
}
|
||||
expect_failures = [var.environment]
|
||||
}
|
||||
|
||||
run "reject_environment_production" {
|
||||
command = plan
|
||||
variables {
|
||||
environment = "production"
|
||||
}
|
||||
expect_failures = [var.environment]
|
||||
}
|
||||
|
||||
# ─── Validation: openresty_source_type must be one of the three modes ─────────
|
||||
|
||||
run "reject_invalid_source_type" {
|
||||
command = plan
|
||||
variables {
|
||||
openresty_source_type = "s3_bucket"
|
||||
}
|
||||
expect_failures = [var.openresty_source_type]
|
||||
}
|
||||
127
variables.tf
Normal file
127
variables.tf
Normal file
@@ -0,0 +1,127 @@
|
||||
# ─── Remote Host (SSH) ────────────────────────────────────────────────────────
|
||||
|
||||
variable "ssh_host" {
|
||||
description = "Hostname or IP of the remote Docker host."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "ssh_user" {
|
||||
description = "SSH user on the remote Docker host."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "ssh_key_path" {
|
||||
description = "Absolute path to the SSH private key file used to connect to the remote host."
|
||||
type = string
|
||||
}
|
||||
|
||||
# ─── App Identity ─────────────────────────────────────────────────────────────
|
||||
|
||||
variable "app_name" {
|
||||
description = "Unique name for this app deployment. Used as prefix for all resource names."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "environment" {
|
||||
description = "Deployment environment: 'prod' (persistent volumes) or 'dev' (ephemeral)."
|
||||
type = string
|
||||
default = "dev"
|
||||
}
|
||||
|
||||
# ─── OpenResty ────────────────────────────────────────────────────────────────
|
||||
|
||||
variable "openresty_source_type" {
|
||||
description = "OpenResty config source: 'bind_mount', 'local_build', or 'git_clone'."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "openresty_image" {
|
||||
description = "Base OpenResty Docker image (bind_mount and git_clone modes)."
|
||||
type = string
|
||||
default = "openresty/openresty:1.25.3-alpine"
|
||||
}
|
||||
|
||||
variable "openresty_external_port" {
|
||||
description = "External port on the remote host that maps to OpenResty port 80."
|
||||
type = number
|
||||
}
|
||||
|
||||
# bind_mount
|
||||
variable "openresty_remote_config_path" {
|
||||
description = "Path on the REMOTE HOST to mount as OpenResty config dir (bind_mount mode)."
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
# local_build
|
||||
variable "openresty_local_build_context" {
|
||||
description = "Local path to Dockerfile build context directory (local_build mode)."
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "openresty_dockerfile" {
|
||||
description = "Dockerfile filename within the build context (local_build mode)."
|
||||
type = string
|
||||
default = "Dockerfile"
|
||||
}
|
||||
|
||||
# git_clone
|
||||
variable "openresty_git_repo" {
|
||||
description = "Git repository URL to clone at container startup (git_clone mode)."
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "openresty_git_ref" {
|
||||
description = "Git ref (tag, branch, commit SHA) to checkout (git_clone mode). Must be a pinned tag or SHA, not a branch name."
|
||||
type = string
|
||||
default = ""
|
||||
|
||||
validation {
|
||||
condition = var.openresty_git_ref == "" || !contains(
|
||||
["main", "master", "develop", "dev", "staging", "HEAD", "latest", "trunk"],
|
||||
var.openresty_git_ref
|
||||
)
|
||||
error_message = "openresty_git_ref must be a pinned tag or commit SHA, not a mutable branch name. Got: '${var.openresty_git_ref}'."
|
||||
}
|
||||
}
|
||||
|
||||
variable "openresty_git_token" {
|
||||
description = "Optional auth token for private git repos (git_clone mode)."
|
||||
type = string
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
# ─── PostgreSQL ───────────────────────────────────────────────────────────────
|
||||
|
||||
variable "db_name" {
|
||||
description = "PostgreSQL database name."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "db_user" {
|
||||
description = "PostgreSQL user."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "db_password" {
|
||||
description = "PostgreSQL password."
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "postgres_image" {
|
||||
description = "PostgreSQL Docker image."
|
||||
type = string
|
||||
default = "postgres:16-alpine"
|
||||
}
|
||||
|
||||
# ─── Redis ────────────────────────────────────────────────────────────────────
|
||||
|
||||
variable "redis_image" {
|
||||
description = "Redis Docker image."
|
||||
type = string
|
||||
default = "redis:7-alpine"
|
||||
}
|
||||
Reference in New Issue
Block a user