New Function: DEV VM
Some checks failed
Deploy / Update Proxmox Dev VMs / Detect changed Proxmox tfvars (push) Successful in 26s
Test / Static Analysis (push) Failing after 25s
Test / Unit Tests — Docker Stack (push) Has been skipped
Test / Unit Tests — K8s Stack (push) Has been skipped
Deploy / Update Proxmox Dev VMs / Provision ${{ matrix.tfvars }} (push) Failing after 32s
Deploy / Update Proxmox Dev VMs / Destroy ${{ matrix.tfvars }} (push) Has been skipped
Test / Integration Test — K8s (k3d) (push) Has been skipped
Some checks failed
Deploy / Update Proxmox Dev VMs / Detect changed Proxmox tfvars (push) Successful in 26s
Test / Static Analysis (push) Failing after 25s
Test / Unit Tests — Docker Stack (push) Has been skipped
Test / Unit Tests — K8s Stack (push) Has been skipped
Deploy / Update Proxmox Dev VMs / Provision ${{ matrix.tfvars }} (push) Failing after 32s
Deploy / Update Proxmox Dev VMs / Destroy ${{ matrix.tfvars }} (push) Has been skipped
Test / Integration Test — K8s (k3d) (push) Has been skipped
This commit is contained in:
365
.gitea/workflows/deploy-proxmox.yml
Normal file
365
.gitea/workflows/deploy-proxmox.yml
Normal file
@@ -0,0 +1,365 @@
|
||||
name: Deploy / Update Proxmox Dev VMs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "proxmox/apps/*.tfvars"
|
||||
|
||||
env:
|
||||
TOFU_VERSION: "1.9.0"
|
||||
TOFU_WORKING_DIR: "proxmox"
|
||||
|
||||
jobs:
|
||||
detect-changes:
|
||||
name: Detect changed Proxmox 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 -- 'proxmox/apps/*.tfvars' | jq -R -s -c 'split("\n") | map(select(length > 0))')
|
||||
DELETED=$(git diff --name-only --diff-filter=D HEAD~1 HEAD -- 'proxmox/apps/*.tfvars' | jq -R -s -c 'split("\n") | map(select(length > 0))')
|
||||
echo "added_modified=$ADDED_MODIFIED" >> "$GITHUB_OUTPUT"
|
||||
echo "deleted=$DELETED" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# ─── Provision / Update VM ────────────────────────────────────────────────
|
||||
provision:
|
||||
name: Provision ${{ 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 and openssh-client
|
||||
run: |
|
||||
sudo apt-get install -y -qq openssh-client
|
||||
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 developer name from tfvars filename
|
||||
id: dev
|
||||
run: |
|
||||
DEV=$(basename "${{ matrix.tfvars }}" .tfvars)
|
||||
echo "name=$DEV" >> "$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-proxmox/${{ steps.dev.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.dev.outputs.name }}" \
|
||||
|| tofu workspace new "${{ steps.dev.outputs.name }}"
|
||||
|
||||
- name: tofu apply (provision VM)
|
||||
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="proxmox_endpoint=${{ secrets.PROXMOX_ENDPOINT }}" \
|
||||
-var="proxmox_api_token=${{ secrets.PROXMOX_API_TOKEN }}" \
|
||||
-var="proxmox_tls_insecure=${{ secrets.PROXMOX_TLS_INSECURE || 'false' }}"
|
||||
|
||||
- name: Read VM outputs
|
||||
id: vm
|
||||
working-directory: ${{ env.TOFU_WORKING_DIR }}
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.SEAWEED_ACCESS_KEY }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SEAWEED_SECRET_KEY }}
|
||||
run: |
|
||||
VM_IP=$(tofu output -raw vm_ip)
|
||||
VM_ROLE=$(tofu output -raw vm_role)
|
||||
KEY_GEN=$(tofu output -raw key_was_generated)
|
||||
CI_USER=$(tofu output -raw cloud_init_user)
|
||||
echo "ip=$VM_IP" >> "$GITHUB_OUTPUT"
|
||||
echo "role=$VM_ROLE" >> "$GITHUB_OUTPUT"
|
||||
echo "key_generated=$KEY_GEN" >> "$GITHUB_OUTPUT"
|
||||
echo "ci_user=$CI_USER" >> "$GITHUB_OUTPUT"
|
||||
echo "VM IP: $VM_IP Role: $VM_ROLE Key generated: $KEY_GEN"
|
||||
|
||||
# Store auto-generated SSH key in gopass so subsequent app deploy pipelines
|
||||
# can retrieve it via infra/ssh-keys/<devname> (same convention as Docker stack).
|
||||
- name: Store generated SSH key in gopass
|
||||
if: steps.vm.outputs.key_generated == 'true'
|
||||
working-directory: ${{ env.TOFU_WORKING_DIR }}
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.SEAWEED_ACCESS_KEY }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SEAWEED_SECRET_KEY }}
|
||||
run: |
|
||||
SSH_KEY=$(tofu output -raw ssh_private_key)
|
||||
echo "::add-mask::$SSH_KEY"
|
||||
echo "$SSH_KEY" | gopass insert -f "infra/ssh-keys/${{ steps.dev.outputs.name }}"
|
||||
echo "SSH private key stored in gopass at infra/ssh-keys/${{ steps.dev.outputs.name }}"
|
||||
|
||||
# For k3s VMs: SSH in, wait for k3s to be active, fetch the kubeconfig,
|
||||
# replace the loopback address with the actual VM IP, and store in gopass.
|
||||
- name: Fetch and store k3s kubeconfig
|
||||
if: steps.vm.outputs.role == 'k3s'
|
||||
run: |
|
||||
DEV="${{ steps.dev.outputs.name }}"
|
||||
VM_IP="${{ steps.vm.outputs.ip }}"
|
||||
CI_USER="${{ steps.vm.outputs.ci_user }}"
|
||||
|
||||
gopass show -o "infra/ssh-keys/$DEV" > /tmp/deploy_key
|
||||
chmod 600 /tmp/deploy_key
|
||||
|
||||
echo "Waiting for k3s to become active on $VM_IP..."
|
||||
for i in $(seq 1 30); do
|
||||
if ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 \
|
||||
-i /tmp/deploy_key "$CI_USER@$VM_IP" \
|
||||
"systemctl is-active --quiet k3s" 2>/dev/null; then
|
||||
echo "k3s is active after $i attempt(s)."
|
||||
break
|
||||
fi
|
||||
if [ "$i" -eq 30 ]; then
|
||||
echo "ERROR: k3s did not become active within 5 minutes."
|
||||
exit 1
|
||||
fi
|
||||
echo " Attempt $i/30 — retrying in 10s..."
|
||||
sleep 10
|
||||
done
|
||||
|
||||
ssh -o StrictHostKeyChecking=no -i /tmp/deploy_key "$CI_USER@$VM_IP" \
|
||||
"cat /etc/rancher/k3s/k3s.yaml" \
|
||||
| sed "s|127.0.0.1|$VM_IP|g" \
|
||||
| gopass insert -f "infra/kubeconfigs/$DEV"
|
||||
|
||||
rm -f /tmp/deploy_key
|
||||
echo "kubeconfig stored in gopass at infra/kubeconfigs/$DEV"
|
||||
|
||||
# ─── Chained deploy: apply linked QA app tfvars if present ─────────────
|
||||
# Convention: apps/<devname>-qa.tfvars for Docker, k8s/apps/<devname>-qa.tfvars for k3s.
|
||||
# The -qa suffix makes QA environments discoverable without extra config.
|
||||
|
||||
- name: Check for linked Docker QA app tfvars
|
||||
id: linked_docker
|
||||
if: steps.vm.outputs.role == 'docker'
|
||||
run: |
|
||||
LINKED="apps/${{ steps.dev.outputs.name }}-qa.tfvars"
|
||||
if [ -f "$LINKED" ]; then
|
||||
echo "found=true" >> "$GITHUB_OUTPUT"
|
||||
echo "path=$LINKED" >> "$GITHUB_OUTPUT"
|
||||
echo "Linked Docker QA app found: $LINKED"
|
||||
else
|
||||
echo "found=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No linked Docker QA app at $LINKED — skipping chained deploy."
|
||||
fi
|
||||
|
||||
- name: Check for linked K8s QA app tfvars
|
||||
id: linked_k8s
|
||||
if: steps.vm.outputs.role == 'k3s'
|
||||
run: |
|
||||
LINKED="k8s/apps/${{ steps.dev.outputs.name }}-qa.tfvars"
|
||||
if [ -f "$LINKED" ]; then
|
||||
echo "found=true" >> "$GITHUB_OUTPUT"
|
||||
echo "path=$LINKED" >> "$GITHUB_OUTPUT"
|
||||
echo "Linked K8s QA app found: $LINKED"
|
||||
else
|
||||
echo "found=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No linked K8s QA app at $LINKED — skipping chained deploy."
|
||||
fi
|
||||
|
||||
- name: Chained Docker deploy
|
||||
if: steps.vm.outputs.role == 'docker' && steps.linked_docker.outputs.found == 'true'
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.SEAWEED_ACCESS_KEY }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SEAWEED_SECRET_KEY }}
|
||||
run: |
|
||||
DEV="${{ steps.dev.outputs.name }}"
|
||||
LINKED="${{ steps.linked_docker.outputs.path }}"
|
||||
APP=$(basename "$LINKED" .tfvars)
|
||||
VM_IP="${{ steps.vm.outputs.ip }}"
|
||||
|
||||
echo "── Chained Docker deploy: $APP → $VM_IP ──"
|
||||
|
||||
gopass show -o "infra/ssh-keys/$DEV" > /tmp/deploy_key
|
||||
chmod 600 /tmp/deploy_key
|
||||
|
||||
DB_PASSWORD=$(gopass show -o "apps/$APP/db_password" 2>/dev/null || echo "")
|
||||
GIT_TOKEN=$(gopass show -o "apps/$APP/git_token" 2>/dev/null || echo "")
|
||||
echo "::add-mask::$DB_PASSWORD"
|
||||
echo "::add-mask::$GIT_TOKEN"
|
||||
|
||||
tofu init \
|
||||
-backend-config="bucket=tofu-state" \
|
||||
-backend-config="key=apps/$APP.tfstate" \
|
||||
-backend-config="endpoint=${{ secrets.SEAWEED_S3_ENDPOINT }}" \
|
||||
-backend-config="region=us-east-1" \
|
||||
-backend-config="force_path_style=true"
|
||||
|
||||
tofu workspace select "$APP" || tofu workspace new "$APP"
|
||||
|
||||
tofu apply -auto-approve \
|
||||
-var-file="$LINKED" \
|
||||
-var="ssh_host=$VM_IP" \
|
||||
-var="ssh_key_path=/tmp/deploy_key" \
|
||||
-var="db_password=$DB_PASSWORD" \
|
||||
-var="openresty_git_token=$GIT_TOKEN"
|
||||
|
||||
rm -f /tmp/deploy_key
|
||||
echo "Chained Docker deploy complete: $APP"
|
||||
|
||||
- name: Chained K8s deploy
|
||||
if: steps.vm.outputs.role == 'k3s' && steps.linked_k8s.outputs.found == 'true'
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.SEAWEED_ACCESS_KEY }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SEAWEED_SECRET_KEY }}
|
||||
run: |
|
||||
DEV="${{ steps.dev.outputs.name }}"
|
||||
LINKED="${{ steps.linked_k8s.outputs.path }}"
|
||||
APP=$(basename "$LINKED" .tfvars)
|
||||
|
||||
echo "── Chained K8s deploy: $APP ──"
|
||||
|
||||
gopass show -o "infra/kubeconfigs/$DEV" > /tmp/kubeconfig
|
||||
chmod 600 /tmp/kubeconfig
|
||||
|
||||
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 "")
|
||||
echo "::add-mask::$RABBITMQ_PASSWORD"
|
||||
echo "::add-mask::$LOKI_AUTH_TOKEN"
|
||||
|
||||
cd k8s
|
||||
tofu init \
|
||||
-backend-config="bucket=tofu-state" \
|
||||
-backend-config="key=apps-k8s/$APP.tfstate" \
|
||||
-backend-config="endpoint=${{ secrets.SEAWEED_S3_ENDPOINT }}" \
|
||||
-backend-config="region=us-east-1" \
|
||||
-backend-config="force_path_style=true"
|
||||
|
||||
tofu workspace select "$APP" || tofu workspace new "$APP"
|
||||
|
||||
tofu apply -auto-approve \
|
||||
-var-file="../$LINKED" \
|
||||
-var="kubeconfig_path=/tmp/kubeconfig" \
|
||||
-var="rabbitmq_password=$RABBITMQ_PASSWORD" \
|
||||
-var="loki_auth_token=$LOKI_AUTH_TOKEN"
|
||||
|
||||
rm -f /tmp/kubeconfig
|
||||
echo "Chained K8s deploy complete: $APP"
|
||||
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
run: rm -f /tmp/deploy_key /tmp/kubeconfig
|
||||
|
||||
# ─── Destroy VM (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:
|
||||
# Check out the previous commit so the deleted tfvars file is present.
|
||||
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 developer name
|
||||
id: dev
|
||||
run: |
|
||||
DEV=$(basename "${{ matrix.tfvars }}" .tfvars)
|
||||
echo "name=$DEV" >> "$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-proxmox/${{ steps.dev.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.dev.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="proxmox_endpoint=${{ secrets.PROXMOX_ENDPOINT }}" \
|
||||
-var="proxmox_api_token=${{ secrets.PROXMOX_API_TOKEN }}" \
|
||||
-var="proxmox_tls_insecure=${{ secrets.PROXMOX_TLS_INSECURE || 'false' }}"
|
||||
|
||||
- name: Delete workspace
|
||||
working-directory: ${{ env.TOFU_WORKING_DIR }}
|
||||
run: |
|
||||
tofu workspace select default
|
||||
tofu workspace delete "${{ steps.dev.outputs.name }}"
|
||||
|
||||
- name: Remove credentials from gopass
|
||||
run: |
|
||||
DEV="${{ steps.dev.outputs.name }}"
|
||||
gopass rm -f "infra/ssh-keys/$DEV" 2>/dev/null || true
|
||||
gopass rm -f "infra/kubeconfigs/$DEV" 2>/dev/null || true
|
||||
echo "Removed gopass entries for developer '$DEV'."
|
||||
@@ -1,10 +1,11 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 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
|
||||
# Four 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
|
||||
# deploy-proxmox.gitlab-ci.yml Proxmox dev VM provision/destroy via proxmox/apps/*.tfvars
|
||||
#
|
||||
# Required GitLab CI/CD variables (Settings > CI/CD > Variables):
|
||||
# TOFU_VERSION OpenTofu version to install (default: 1.9.0)
|
||||
@@ -13,14 +14,17 @@
|
||||
# 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)
|
||||
# PROXMOX_ENDPOINT Proxmox API URL, e.g. https://proxmox.home:8006/
|
||||
# PROXMOX_API_TOKEN Proxmox API token: user@pam!tokenid=secret (Masked)
|
||||
# PROXMOX_TLS_INSECURE "true" when using a self-signed cert (optional)
|
||||
#
|
||||
# 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)
|
||||
# infra/ssh-keys/<name> — SSH private key (Docker host or dev VM)
|
||||
# infra/kubeconfigs/<name> — kubeconfig for K8s cluster or k3s VM
|
||||
# apps/<name>/db_password — PostgreSQL password
|
||||
# apps/<name>/git_token — optional git token for git_clone mode
|
||||
# apps/<name>/rabbitmq_password — RabbitMQ password (K8s stack)
|
||||
# apps/<name>/loki_token — optional Loki auth token (K8s stack)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
stages:
|
||||
@@ -37,3 +41,4 @@ include:
|
||||
- local: .gitlab/workflows/test.gitlab-ci.yml
|
||||
- local: .gitlab/workflows/deploy.gitlab-ci.yml
|
||||
- local: .gitlab/workflows/deploy-k8s.gitlab-ci.yml
|
||||
- local: .gitlab/workflows/deploy-proxmox.gitlab-ci.yml
|
||||
|
||||
300
.gitlab/workflows/deploy-proxmox.gitlab-ci.yml
Normal file
300
.gitlab/workflows/deploy-proxmox.gitlab-ci.yml
Normal file
@@ -0,0 +1,300 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Proxmox dev VM deploy pipeline — mirrors .gitea/workflows/deploy-proxmox.yml
|
||||
#
|
||||
# Triggered on pushes to main when files under proxmox/apps/*.tfvars change.
|
||||
# Two jobs run in sequence:
|
||||
# provision-proxmox — loops over added/modified tfvars, provisions/updates VMs,
|
||||
# stores credentials in gopass, runs chained app deploy
|
||||
# when apps/<devname>-qa.tfvars or k8s/apps/<devname>-qa.tfvars exists
|
||||
# destroy-proxmox — loops over deleted tfvars, destroys VMs, removes gopass entries
|
||||
#
|
||||
# Required GitLab CI/CD variables (in addition to the shared ones):
|
||||
# PROXMOX_ENDPOINT — https://proxmox.example.com:8006/
|
||||
# PROXMOX_API_TOKEN — user@pam!tokenid=xxxxxxxx-... (mark as Masked)
|
||||
# PROXMOX_TLS_INSECURE — "true" when using a self-signed cert (optional)
|
||||
#
|
||||
# State key pattern: apps-proxmox/<devname>.tfstate
|
||||
# gopass paths written by this pipeline:
|
||||
# infra/ssh-keys/<devname> — SSH private key (if auto-generated)
|
||||
# infra/kubeconfigs/<devname> — k3s kubeconfig (if vm_role = "k3s")
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
.proxmox_base:
|
||||
image: ubuntu:22.04
|
||||
variables:
|
||||
GIT_DEPTH: "0"
|
||||
before_script:
|
||||
- apt-get update -qq && apt-get install -y -qq curl git jq gnupg openssh-client
|
||||
- |
|
||||
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"
|
||||
|
||||
# ─── Provision / Update ───────────────────────────────────────────────────────
|
||||
|
||||
provision-proxmox:
|
||||
extends: .proxmox_base
|
||||
stage: deploy
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE == "push"'
|
||||
changes:
|
||||
- proxmox/apps/*.tfvars
|
||||
script:
|
||||
- |
|
||||
ADDED_MODIFIED=$(git diff --name-only --diff-filter=ACM \
|
||||
"$CI_COMMIT_BEFORE_SHA" "$CI_COMMIT_SHA" -- 'proxmox/apps/*.tfvars')
|
||||
|
||||
if [ -z "$ADDED_MODIFIED" ]; then
|
||||
echo "No Proxmox tfvars files added or modified. Nothing to provision."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Files to provision: $ADDED_MODIFIED"
|
||||
FAILED=0
|
||||
|
||||
for TFVARS in $ADDED_MODIFIED; do
|
||||
DEV=$(basename "$TFVARS" .tfvars)
|
||||
echo ""
|
||||
echo "══════════════════════════════════════════"
|
||||
echo " Provisioning VM: $DEV"
|
||||
echo "══════════════════════════════════════════"
|
||||
|
||||
if ! (
|
||||
set -e
|
||||
|
||||
export AWS_ACCESS_KEY_ID="$SEAWEED_ACCESS_KEY"
|
||||
export AWS_SECRET_ACCESS_KEY="$SEAWEED_SECRET_KEY"
|
||||
TLS_INSECURE="${PROXMOX_TLS_INSECURE:-false}"
|
||||
|
||||
# ── tofu init with SeaweedFS backend ──
|
||||
cd proxmox
|
||||
tofu init \
|
||||
-backend-config="bucket=tofu-state" \
|
||||
-backend-config="key=apps-proxmox/$DEV.tfstate" \
|
||||
-backend-config="endpoint=$SEAWEED_S3_ENDPOINT" \
|
||||
-backend-config="region=us-east-1" \
|
||||
-backend-config="force_path_style=true" \
|
||||
-reconfigure
|
||||
|
||||
tofu workspace select "$DEV" 2>/dev/null || tofu workspace new "$DEV"
|
||||
|
||||
tofu apply -auto-approve \
|
||||
-var-file="../$TFVARS" \
|
||||
-var="proxmox_endpoint=$PROXMOX_ENDPOINT" \
|
||||
-var="proxmox_api_token=$PROXMOX_API_TOKEN" \
|
||||
-var="proxmox_tls_insecure=$TLS_INSECURE"
|
||||
|
||||
# ── Read VM outputs ──
|
||||
VM_IP=$(tofu output -raw vm_ip)
|
||||
VM_ROLE=$(tofu output -raw vm_role)
|
||||
KEY_GEN=$(tofu output -raw key_was_generated)
|
||||
CI_USER=$(tofu output -raw cloud_init_user)
|
||||
echo " VM IP: $VM_IP Role: $VM_ROLE Key generated: $KEY_GEN"
|
||||
cd ..
|
||||
|
||||
# ── Store auto-generated SSH key in gopass ──
|
||||
if [ "$KEY_GEN" = "true" ]; then
|
||||
cd proxmox
|
||||
SSH_KEY=$(tofu output -raw ssh_private_key)
|
||||
echo "$SSH_KEY" | gopass insert -f "infra/ssh-keys/$DEV"
|
||||
echo " SSH key stored in gopass at infra/ssh-keys/$DEV"
|
||||
cd ..
|
||||
fi
|
||||
|
||||
# ── For k3s: wait for k3s and fetch kubeconfig ──
|
||||
if [ "$VM_ROLE" = "k3s" ]; then
|
||||
gopass show -o "infra/ssh-keys/$DEV" > /tmp/deploy_key
|
||||
chmod 600 /tmp/deploy_key
|
||||
|
||||
echo " Waiting for k3s to become active on $VM_IP..."
|
||||
for i in $(seq 1 30); do
|
||||
if ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 \
|
||||
-i /tmp/deploy_key "$CI_USER@$VM_IP" \
|
||||
"systemctl is-active --quiet k3s" 2>/dev/null; then
|
||||
echo " k3s is active."
|
||||
break
|
||||
fi
|
||||
if [ "$i" -eq 30 ]; then
|
||||
echo "ERROR: k3s did not become active within 5 minutes."
|
||||
exit 1
|
||||
fi
|
||||
echo " Attempt $i/30 — retrying in 10s..."
|
||||
sleep 10
|
||||
done
|
||||
|
||||
ssh -o StrictHostKeyChecking=no -i /tmp/deploy_key "$CI_USER@$VM_IP" \
|
||||
"cat /etc/rancher/k3s/k3s.yaml" \
|
||||
| sed "s|127.0.0.1|$VM_IP|g" \
|
||||
| gopass insert -f "infra/kubeconfigs/$DEV"
|
||||
|
||||
rm -f /tmp/deploy_key
|
||||
echo " kubeconfig stored in gopass at infra/kubeconfigs/$DEV"
|
||||
fi
|
||||
|
||||
# ── Chained Docker deploy ──
|
||||
if [ "$VM_ROLE" = "docker" ] && [ -f "apps/${DEV}-qa.tfvars" ]; then
|
||||
APP="${DEV}-qa"
|
||||
echo " Chained Docker deploy: $APP → $VM_IP"
|
||||
|
||||
gopass show -o "infra/ssh-keys/$DEV" > /tmp/deploy_key
|
||||
chmod 600 /tmp/deploy_key
|
||||
|
||||
DB_PASSWORD=$(gopass show -o "apps/$APP/db_password" 2>/dev/null || echo "")
|
||||
GIT_TOKEN=$(gopass show -o "apps/$APP/git_token" 2>/dev/null || echo "")
|
||||
|
||||
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
|
||||
|
||||
tofu workspace select "$APP" 2>/dev/null || tofu workspace new "$APP"
|
||||
|
||||
tofu apply -auto-approve \
|
||||
-var-file="apps/${DEV}-qa.tfvars" \
|
||||
-var="ssh_host=$VM_IP" \
|
||||
-var="ssh_key_path=/tmp/deploy_key" \
|
||||
-var="db_password=$DB_PASSWORD" \
|
||||
-var="openresty_git_token=$GIT_TOKEN"
|
||||
|
||||
rm -f /tmp/deploy_key
|
||||
echo " Chained Docker deploy complete: $APP"
|
||||
fi
|
||||
|
||||
# ── Chained K8s deploy ──
|
||||
if [ "$VM_ROLE" = "k3s" ] && [ -f "k8s/apps/${DEV}-qa.tfvars" ]; then
|
||||
APP="${DEV}-qa"
|
||||
echo " Chained K8s deploy: $APP"
|
||||
|
||||
gopass show -o "infra/kubeconfigs/$DEV" > /tmp/kubeconfig
|
||||
chmod 600 /tmp/kubeconfig
|
||||
|
||||
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 "")
|
||||
|
||||
cd k8s
|
||||
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
|
||||
|
||||
tofu workspace select "$APP" 2>/dev/null || tofu workspace new "$APP"
|
||||
|
||||
tofu apply -auto-approve \
|
||||
-var-file="../k8s/apps/${DEV}-qa.tfvars" \
|
||||
-var="kubeconfig_path=/tmp/kubeconfig" \
|
||||
-var="rabbitmq_password=$RABBITMQ_PASSWORD" \
|
||||
-var="loki_auth_token=$LOKI_AUTH_TOKEN"
|
||||
|
||||
cd ..
|
||||
rm -f /tmp/kubeconfig
|
||||
echo " Chained K8s deploy complete: $APP"
|
||||
fi
|
||||
|
||||
echo " Provisioned: $DEV"
|
||||
); then
|
||||
echo "ERROR: Provisioning of $DEV failed — continuing with remaining VMs"
|
||||
FAILED=1
|
||||
rm -f /tmp/deploy_key /tmp/kubeconfig
|
||||
fi
|
||||
done
|
||||
|
||||
if [ $FAILED -ne 0 ]; then
|
||||
echo ""
|
||||
echo "One or more VM provisioning operations failed. See logs above for details."
|
||||
exit 1
|
||||
fi
|
||||
after_script:
|
||||
- rm -f /tmp/deploy_key /tmp/kubeconfig
|
||||
|
||||
# ─── Destroy (tfvars file deleted) ───────────────────────────────────────────
|
||||
|
||||
destroy-proxmox:
|
||||
extends: .proxmox_base
|
||||
stage: destroy
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE == "push"'
|
||||
changes:
|
||||
- proxmox/apps/*.tfvars
|
||||
script:
|
||||
- |
|
||||
DELETED=$(git diff --name-only --diff-filter=D \
|
||||
"$CI_COMMIT_BEFORE_SHA" "$CI_COMMIT_SHA" -- 'proxmox/apps/*.tfvars')
|
||||
|
||||
if [ -z "$DELETED" ]; then
|
||||
echo "No Proxmox tfvars files deleted. Nothing to destroy."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Files to destroy: $DELETED"
|
||||
FAILED=0
|
||||
|
||||
for TFVARS in $DELETED; do
|
||||
DEV=$(basename "$TFVARS" .tfvars)
|
||||
echo ""
|
||||
echo "══════════════════════════════════════════"
|
||||
echo " Destroying VM: $DEV"
|
||||
echo "══════════════════════════════════════════"
|
||||
|
||||
if ! (
|
||||
set -e
|
||||
|
||||
# Recover deleted tfvars from git history
|
||||
git show "$CI_COMMIT_BEFORE_SHA:$TFVARS" > /tmp/${DEV}.tfvars
|
||||
|
||||
export AWS_ACCESS_KEY_ID="$SEAWEED_ACCESS_KEY"
|
||||
export AWS_SECRET_ACCESS_KEY="$SEAWEED_SECRET_KEY"
|
||||
TLS_INSECURE="${PROXMOX_TLS_INSECURE:-false}"
|
||||
|
||||
cd proxmox
|
||||
tofu init \
|
||||
-backend-config="bucket=tofu-state" \
|
||||
-backend-config="key=apps-proxmox/$DEV.tfstate" \
|
||||
-backend-config="endpoint=$SEAWEED_S3_ENDPOINT" \
|
||||
-backend-config="region=us-east-1" \
|
||||
-backend-config="force_path_style=true" \
|
||||
-reconfigure
|
||||
|
||||
tofu workspace select "$DEV"
|
||||
|
||||
tofu destroy -auto-approve \
|
||||
-var-file="/tmp/${DEV}.tfvars" \
|
||||
-var="proxmox_endpoint=$PROXMOX_ENDPOINT" \
|
||||
-var="proxmox_api_token=$PROXMOX_API_TOKEN" \
|
||||
-var="proxmox_tls_insecure=$TLS_INSECURE"
|
||||
|
||||
tofu workspace select default
|
||||
tofu workspace delete "$DEV"
|
||||
cd ..
|
||||
|
||||
# Remove credentials from gopass
|
||||
gopass rm -f "infra/ssh-keys/$DEV" 2>/dev/null || true
|
||||
gopass rm -f "infra/kubeconfigs/$DEV" 2>/dev/null || true
|
||||
|
||||
rm -f "/tmp/${DEV}.tfvars"
|
||||
echo " Destroyed: $DEV"
|
||||
); then
|
||||
echo "ERROR: Destroy of $DEV failed — continuing with remaining VMs"
|
||||
FAILED=1
|
||||
rm -f "/tmp/${DEV}.tfvars"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ $FAILED -ne 0 ]; then
|
||||
echo ""
|
||||
echo "One or more VM destroy operations failed. See logs above for details."
|
||||
exit 1
|
||||
fi
|
||||
after_script:
|
||||
- rm -f /tmp/deploy_key /tmp/kubeconfig /tmp/*.tfvars
|
||||
15
Makefile
15
Makefile
@@ -1,5 +1,5 @@
|
||||
.PHONY: help validate fmt fmt-fix test-unit test-unit-docker test-unit-k8s \
|
||||
test-integration-docker test-integration-k8s test-all clean
|
||||
test-unit-proxmox test-integration-docker test-integration-k8s test-all clean
|
||||
|
||||
TOFU ?= tofu
|
||||
TFLINT ?= tflint
|
||||
@@ -21,7 +21,7 @@ help:
|
||||
## Static analysis (no infrastructure required)
|
||||
##─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
## validate Run 'tofu validate' on both stacks
|
||||
## validate Run 'tofu validate' on all stacks
|
||||
validate:
|
||||
@echo "── Validating Docker stack ──"
|
||||
$(TOFU) init -backend=false -input=false > /dev/null
|
||||
@@ -29,6 +29,9 @@ validate:
|
||||
@echo "── Validating K8s stack ──"
|
||||
cd k8s && $(TOFU) init -backend=false -input=false > /dev/null
|
||||
cd k8s && $(TOFU) validate
|
||||
@echo "── Validating Proxmox stack ──"
|
||||
cd proxmox && $(TOFU) init -backend=false -input=false > /dev/null
|
||||
cd proxmox && $(TOFU) validate
|
||||
@echo "All configurations valid."
|
||||
|
||||
## fmt-check Check HCL formatting in all .tf files
|
||||
@@ -62,8 +65,14 @@ test-unit-k8s:
|
||||
cd k8s && $(TOFU) init -backend=false -input=false > /dev/null
|
||||
cd k8s && $(TOFU) test
|
||||
|
||||
## test-unit-proxmox Run Proxmox stack unit tests (tofu test)
|
||||
test-unit-proxmox:
|
||||
@echo "── Proxmox stack unit tests ──"
|
||||
cd proxmox && $(TOFU) init -backend=false -input=false > /dev/null
|
||||
cd proxmox && $(TOFU) test
|
||||
|
||||
## test-unit Run all unit tests
|
||||
test-unit: test-unit-docker test-unit-k8s
|
||||
test-unit: test-unit-docker test-unit-k8s test-unit-proxmox
|
||||
|
||||
##─────────────────────────────────────────────────────────────────────────────
|
||||
## Integration tests (real local infrastructure)
|
||||
|
||||
463
README.md
463
README.md
@@ -32,15 +32,23 @@ Two CI/CD systems are supported:
|
||||
- [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)
|
||||
9. [Stack: Proxmox — Developer VMs](#stack-proxmox--developer-vms)
|
||||
- [How It Works](#how-it-works-proxmox)
|
||||
- [Proxmox Prerequisites](#proxmox-prerequisites)
|
||||
- [Variables Reference](#variables-reference-proxmox)
|
||||
- [Creating a New Developer VM](#creating-a-new-developer-vm)
|
||||
- [Chained App Deploy](#chained-app-deploy)
|
||||
- [Decommissioning a Developer VM](#decommissioning-a-developer-vm)
|
||||
10. [CI/CD Pipelines](#cicd-pipelines)
|
||||
- [Docker Pipeline](#docker-pipeline)
|
||||
- [Kubernetes Pipeline](#kubernetes-pipeline)
|
||||
- [Proxmox Pipeline](#proxmox-pipeline)
|
||||
- [Destroy on Delete](#destroy-on-delete)
|
||||
- [Gitea vs GitLab differences](#gitea-vs-gitlab-differences)
|
||||
11. [Running Locally](#running-locally)
|
||||
12. [Adding a New Stack Module](#adding-a-new-stack-module)
|
||||
13. [Testing](#testing)
|
||||
14. [Caveats and Known Limitations](#caveats-and-known-limitations)
|
||||
|
||||
---
|
||||
|
||||
@@ -49,21 +57,27 @@ Two CI/CD systems are supported:
|
||||
```
|
||||
Gitea repo
|
||||
│
|
||||
├── apps/*.tfvars ← Docker app configs
|
||||
└── k8s/apps/*.tfvars ← Kubernetes app configs
|
||||
├── apps/*.tfvars ← Docker app configs
|
||||
├── k8s/apps/*.tfvars ← Kubernetes app configs
|
||||
└── proxmox/apps/*.tfvars ← Developer VM configs
|
||||
|
||||
Push to main
|
||||
│
|
||||
├── deploy.yml ← detects apps/*.tfvars changes
|
||||
│ └── tofu apply ← connects to remote Docker host via SSH
|
||||
├── 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
|
||||
├── deploy-k8s.yml ← detects k8s/apps/*.tfvars changes
|
||||
│ └── tofu apply ← connects to K8s cluster via kubeconfig
|
||||
│
|
||||
└── deploy-proxmox.yml ← detects proxmox/apps/*.tfvars changes
|
||||
└── tofu apply ← provisions Proxmox VM via API token
|
||||
├── stores credentials in gopass
|
||||
└── (optional) tofu apply → deploys app onto the new VM
|
||||
│
|
||||
└── Per-app workspace in SeaweedFS state bucket
|
||||
└── Per-app/VM workspace in SeaweedFS state bucket
|
||||
```
|
||||
|
||||
Each `.tfvars` file = one app deployment. One Tofu workspace = one isolated state.
|
||||
Each `.tfvars` file = one 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.
|
||||
|
||||
@@ -78,10 +92,16 @@ runtime and passed to `tofu apply` via `-var` flags.
|
||||
│ │ ├── variables.tf
|
||||
│ │ ├── main.tf
|
||||
│ │ └── outputs.tf
|
||||
│ └── app-k8s-nodered-rabbitmq/ # Kubernetes stack module
|
||||
│ ├── app-k8s-nodered-rabbitmq/ # Kubernetes stack module
|
||||
│ │ ├── variables.tf
|
||||
│ │ ├── main.tf
|
||||
│ │ └── outputs.tf
|
||||
│ └── vm-proxmox/ # Proxmox developer VM module
|
||||
│ ├── variables.tf
|
||||
│ ├── main.tf
|
||||
│ └── outputs.tf
|
||||
│ ├── outputs.tf
|
||||
│ ├── cloud-init-docker.yaml.tftpl
|
||||
│ └── cloud-init-k3s.yaml.tftpl
|
||||
│
|
||||
├── apps/ # Docker app configs (one file per app)
|
||||
│ ├── example-dev.tfvars
|
||||
@@ -103,6 +123,15 @@ runtime and passed to `tofu apply` via `-var` flags.
|
||||
│ └── tests/
|
||||
│ └── k8s_validation.tftest.hcl # K8s stack unit tests (tofu test)
|
||||
│
|
||||
├── proxmox/ # Proxmox stack root
|
||||
│ ├── main.tf # Proxmox provider + module call
|
||||
│ ├── variables.tf
|
||||
│ ├── backend.tf
|
||||
│ ├── apps/ # Developer VM configs (one file per developer)
|
||||
│ │ └── example-developer.tfvars
|
||||
│ └── tests/
|
||||
│ └── proxmox_validation.tftest.hcl # Proxmox stack unit tests (tofu test)
|
||||
│
|
||||
├── scripts/
|
||||
│ └── setup-backend.sh # One-time SeaweedFS backend setup
|
||||
│
|
||||
@@ -112,6 +141,7 @@ runtime and passed to `tofu apply` via `-var` flags.
|
||||
│ └── workflows/
|
||||
│ ├── deploy.yml # Docker CI/CD pipeline (Gitea Actions)
|
||||
│ ├── deploy-k8s.yml # Kubernetes CI/CD pipeline (Gitea Actions)
|
||||
│ ├── deploy-proxmox.yml # Proxmox VM pipeline (Gitea Actions)
|
||||
│ └── test.yml # Test pipeline (Gitea Actions)
|
||||
│
|
||||
└── .gitlab/
|
||||
@@ -119,6 +149,7 @@ runtime and passed to `tofu apply` via `-var` flags.
|
||||
└── workflows/
|
||||
├── deploy.gitlab-ci.yml # Docker CI/CD pipeline (GitLab CI)
|
||||
├── deploy-k8s.gitlab-ci.yml # Kubernetes CI/CD pipeline (GitLab CI)
|
||||
├── deploy-proxmox.gitlab-ci.yml # Proxmox VM pipeline (GitLab CI)
|
||||
└── test.gitlab-ci.yml # Test pipeline (GitLab CI)
|
||||
```
|
||||
|
||||
@@ -176,6 +207,7 @@ access key and secret key.
|
||||
|-------|------------------|
|
||||
| Docker | `apps/<appname>.tfstate` |
|
||||
| Kubernetes | `apps-k8s/<appname>.tfstate` |
|
||||
| Proxmox | `apps-proxmox/<devname>.tfstate` |
|
||||
|
||||
### 4. Initialise the backend (automated)
|
||||
|
||||
@@ -226,9 +258,9 @@ gopass store
|
||||
│
|
||||
├── infra/
|
||||
│ ├── ssh-keys/
|
||||
│ │ └── <appname> # SSH private key for the remote Docker host
|
||||
│ │ └── <name> # SSH private key — Docker host or developer VM
|
||||
│ └── kubeconfigs/
|
||||
│ └── <appname> # kubeconfig for the target K8s cluster
|
||||
│ └── <name> # kubeconfig — K8s cluster or developer k3s VM
|
||||
│
|
||||
└── apps/
|
||||
└── <appname>/
|
||||
@@ -238,6 +270,11 @@ gopass store
|
||||
└── loki_token # Loki bearer token (K8s stack, optional)
|
||||
```
|
||||
|
||||
> **Proxmox note:** The Proxmox pipeline writes `infra/ssh-keys/<devname>` and
|
||||
> `infra/kubeconfigs/<devname>` automatically — no manual `gopass insert` needed for
|
||||
> the VM credentials. App-level secrets (e.g. `apps/<devname>-qa/db_password`) still
|
||||
> need to be added manually before the chained deploy runs.
|
||||
|
||||
### Adding a secret
|
||||
|
||||
```sh
|
||||
@@ -257,12 +294,15 @@ For SSH keys specifically: update `infra/ssh-keys/<appname>` in gopass and updat
|
||||
|
||||
## CI/CD Variables
|
||||
|
||||
The same five values are required regardless of which CI/CD system you use.
|
||||
The same base values are required regardless of which CI/CD system you use.
|
||||
Add the Proxmox group when using the developer VM stack.
|
||||
|
||||
### Gitea Repository Secrets
|
||||
|
||||
Configure in **Gitea → Repository → Settings → Secrets**.
|
||||
|
||||
#### Base (all stacks)
|
||||
|
||||
| Secret | Purpose |
|
||||
|--------|---------|
|
||||
| `GOPASS_GPG_KEY` | GPG private key (armored) to decrypt the gopass store |
|
||||
@@ -271,11 +311,21 @@ Configure in **Gitea → Repository → Settings → Secrets**.
|
||||
| `SEAWEED_ACCESS_KEY` | SeaweedFS S3 access key |
|
||||
| `SEAWEED_SECRET_KEY` | SeaweedFS S3 secret key (mark as masked) |
|
||||
|
||||
#### Proxmox stack (add when using developer VMs)
|
||||
|
||||
| Secret | Purpose |
|
||||
|--------|---------|
|
||||
| `PROXMOX_ENDPOINT` | Proxmox API URL, e.g. `https://proxmox.example.com:8006/` |
|
||||
| `PROXMOX_API_TOKEN` | API token in `user@realm!tokenid=secret` format (mark as masked) |
|
||||
| `PROXMOX_TLS_INSECURE` | `"true"` when using a self-signed TLS certificate (optional) |
|
||||
|
||||
### GitLab CI/CD Variables
|
||||
|
||||
Configure in **GitLab → Project → Settings → CI/CD → Variables**. Mark sensitive values
|
||||
as **Masked** so they are redacted from job logs.
|
||||
|
||||
#### Base (all stacks)
|
||||
|
||||
| Variable | Masked | Purpose |
|
||||
|----------|--------|---------|
|
||||
| `GOPASS_GPG_KEY` | yes | GPG private key (armored) to decrypt the gopass store |
|
||||
@@ -284,6 +334,14 @@ as **Masked** so they are redacted from job logs.
|
||||
| `SEAWEED_ACCESS_KEY` | yes | SeaweedFS S3 access key |
|
||||
| `SEAWEED_SECRET_KEY` | yes | SeaweedFS S3 secret key |
|
||||
|
||||
#### Proxmox stack (add when using developer VMs)
|
||||
|
||||
| Variable | Masked | Purpose |
|
||||
|----------|--------|---------|
|
||||
| `PROXMOX_ENDPOINT` | no | Proxmox API URL, e.g. `https://proxmox.example.com:8006/` |
|
||||
| `PROXMOX_API_TOKEN` | yes | API token in `user@realm!tokenid=secret` format |
|
||||
| `PROXMOX_TLS_INSECURE` | no | `"true"` when using a self-signed TLS certificate (optional) |
|
||||
|
||||
> **Note:** GitLab CI variables are plain environment variables — no `${{ secrets.X }}`
|
||||
> syntax is needed. The pipelines reference them directly as `$VARIABLE_NAME`.
|
||||
|
||||
@@ -642,15 +700,333 @@ The Traefik entrypoint (default: `"web"`) is configurable per app via `traefik_e
|
||||
|
||||
---
|
||||
|
||||
## Stack: Proxmox — Developer VMs
|
||||
|
||||
Provisions persistent developer VMs on a **Proxmox VE** host via the Proxmox API.
|
||||
Each developer gets an isolated VM that can run either Docker (for the Docker stack)
|
||||
or k3s (for the Kubernetes stack), giving them a full QA environment that mirrors
|
||||
the production deployment pipeline.
|
||||
|
||||
### How It Works (Proxmox)
|
||||
|
||||
```
|
||||
proxmox/apps/<devname>.tfvars → pipeline provisions VM on Proxmox
|
||||
│
|
||||
├── clones a cloud-init template
|
||||
├── installs Docker CE (vm_role = "docker")
|
||||
│ or k3s (vm_role = "k3s")
|
||||
├── injects developer SSH key
|
||||
├── stores credentials in gopass automatically
|
||||
│ infra/ssh-keys/<devname>
|
||||
│ infra/kubeconfigs/<devname> (k3s only)
|
||||
│
|
||||
└── (optional) chained deploy
|
||||
apps/<devname>-qa.tfvars (docker)
|
||||
k8s/apps/<devname>-qa.tfvars (k3s)
|
||||
```
|
||||
|
||||
The VM hostname and all gopass key names are derived from the tfvars filename
|
||||
(`proxmox/apps/alice.tfvars` → developer name `alice`).
|
||||
|
||||
**IP address:** assigned by DHCP, read from the QEMU guest agent after boot.
|
||||
With a persistent VM and stable DHCP leases this IP does not change day-to-day,
|
||||
but static DHCP reservations on your router/DHCP server are recommended for production use.
|
||||
|
||||
### Proxmox Prerequisites
|
||||
|
||||
Before using this stack, complete the following one-time setup on Proxmox:
|
||||
|
||||
#### 1. Cloud-init template
|
||||
|
||||
The module clones an existing VM template. The template must:
|
||||
|
||||
- Be a Linux VM (Debian 12 or Ubuntu 22.04 recommended)
|
||||
- Have `cloud-init` and `qemu-guest-agent` installed and enabled
|
||||
- Have the cloud-init drive attached (`--ide2 local-lvm:cloudinit` or equivalent)
|
||||
|
||||
Create a template from a cloud image (example for Debian 12):
|
||||
|
||||
```sh
|
||||
# On the Proxmox host
|
||||
VMID=9000
|
||||
wget https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2
|
||||
|
||||
qm create $VMID --name debian12-cloud --memory 1024 --cores 1 --net0 virtio,bridge=vmbr0
|
||||
qm importdisk $VMID debian-12-genericcloud-amd64.qcow2 local-lvm
|
||||
qm set $VMID --scsihw virtio-scsi-pci --scsi0 local-lvm:vm-${VMID}-disk-0
|
||||
qm set $VMID --ide2 local-lvm:cloudinit --boot c --bootdisk scsi0
|
||||
qm set $VMID --serial0 socket --vga serial0
|
||||
qm set $VMID --agent enabled=1
|
||||
|
||||
# Install qemu-guest-agent inside the VM, then convert to template:
|
||||
qm template $VMID
|
||||
```
|
||||
|
||||
Note the VM ID (`9000` in the example) — this is your `template_vm_id` in the tfvars.
|
||||
|
||||
#### 2. Enable Snippets on a datastore
|
||||
|
||||
Cloud-init user-data (the Docker/k3s install script) is uploaded as a Proxmox snippet.
|
||||
At least one datastore must have the **Snippets** content type enabled.
|
||||
|
||||
Proxmox UI → **Datacenter → Storage → `local` → Edit → Content** → check **Snippets**.
|
||||
|
||||
The default `snippets_datastore = "local"` in the tfvars assumes the `local` datastore.
|
||||
|
||||
#### 3. Create an API token
|
||||
|
||||
Proxmox UI → **Datacenter → Permissions → API Tokens → Add**
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| User | `root@pam` (or a dedicated CI user) |
|
||||
| Token ID | `ci` (or any name you choose) |
|
||||
| Privilege Separation | unchecked (inherits user permissions) |
|
||||
|
||||
Copy the displayed token secret — it is shown only once. The full token string has the
|
||||
form `root@pam!ci=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`.
|
||||
|
||||
Store it in the Gitea secret `PROXMOX_API_TOKEN` and configure `PROXMOX_ENDPOINT` to
|
||||
point at your Proxmox host, e.g. `https://proxmox.home:8006/`.
|
||||
|
||||
Set `PROXMOX_TLS_INSECURE=true` if your Proxmox uses a self-signed certificate.
|
||||
|
||||
### Variables Reference (Proxmox)
|
||||
|
||||
#### Connection (injected at CI runtime — never in tfvars)
|
||||
|
||||
| Variable | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `proxmox_endpoint` | string | Proxmox API URL, e.g. `https://proxmox.example.com:8006/` |
|
||||
| `proxmox_api_token` | string | API token `user@realm!tokenid=secret` (sensitive) |
|
||||
| `proxmox_tls_insecure` | bool | Skip TLS verification (default: `false`) |
|
||||
|
||||
#### Placement
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `proxmox_node` | string | — | Proxmox node name, e.g. `"pve"` |
|
||||
| `template_vm_id` | number | — | VM ID of the cloud-init template to clone |
|
||||
| `storage_pool` | string | — | Disk storage pool, e.g. `"local-lvm"` |
|
||||
| `snippets_datastore` | string | `"local"` | Datastore with Snippets content enabled |
|
||||
| `vm_id` | number | `0` | Explicit Proxmox VM ID; `0` = auto-assign |
|
||||
|
||||
#### Sizing
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `cores` | number | `2` | vCPU cores |
|
||||
| `memory_mb` | number | `2048` | RAM in megabytes |
|
||||
| `disk_size_gb` | number | `20` | Root disk size in gigabytes |
|
||||
|
||||
#### Networking
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `bridge` | string | `"vmbr0"` | Proxmox network bridge |
|
||||
| `vlan_tag` | number | `null` | VLAN tag for the VM NIC; `null` = untagged |
|
||||
|
||||
#### Role & access
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `vm_role` | string | — | `"docker"` or `"k3s"` |
|
||||
| `cloud_init_user` | string | `"developer"` | Username created by cloud-init |
|
||||
| `ssh_public_key` | string | `""` | Developer's SSH public key. Empty = auto-generate |
|
||||
|
||||
### Creating a New Developer VM
|
||||
|
||||
This is the standard workflow for onboarding a developer onto their own QA environment.
|
||||
|
||||
#### Step 1 — Add Proxmox secrets to Gitea (one-time, per repository)
|
||||
|
||||
If not already done, add to **Gitea → Repository → Settings → Secrets**:
|
||||
|
||||
```
|
||||
PROXMOX_ENDPOINT https://proxmox.example.com:8006/
|
||||
PROXMOX_API_TOKEN root@pam!ci=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||
PROXMOX_TLS_INSECURE true # only if using a self-signed cert
|
||||
```
|
||||
|
||||
#### Step 2 — Create the developer VM tfvars
|
||||
|
||||
Copy `proxmox/apps/example-developer.tfvars` to `proxmox/apps/<devname>.tfvars`
|
||||
and fill in the values for your environment:
|
||||
|
||||
```hcl
|
||||
# proxmox/apps/alice.tfvars
|
||||
|
||||
proxmox_node = "pve"
|
||||
template_vm_id = 9000
|
||||
storage_pool = "local-lvm"
|
||||
|
||||
cores = 2
|
||||
memory_mb = 4096
|
||||
disk_size_gb = 40
|
||||
|
||||
bridge = "vmbr0"
|
||||
# vlan_tag = 100 # optional
|
||||
|
||||
vm_role = "docker" # "docker" or "k3s"
|
||||
cloud_init_user = "developer"
|
||||
|
||||
# Option A — let Tofu generate an ED25519 key pair (recommended):
|
||||
ssh_public_key = ""
|
||||
|
||||
# Option B — provide the developer's own public key:
|
||||
# ssh_public_key = "ssh-ed25519 AAAA... alice@laptop"
|
||||
```
|
||||
|
||||
#### Step 3 — (Optional) Add app-level secrets to gopass
|
||||
|
||||
If you also want the chained deploy to run (deploying an app onto the new VM automatically),
|
||||
add the app secrets **before** pushing. The app name follows the convention `<devname>-qa`:
|
||||
|
||||
For a Docker app (`apps/alice-qa.tfvars`):
|
||||
```sh
|
||||
gopass insert apps/alice-qa/db_password
|
||||
gopass insert apps/alice-qa/git_token # only if using git_clone mode
|
||||
```
|
||||
|
||||
For a K8s app (`k8s/apps/alice-qa.tfvars`):
|
||||
```sh
|
||||
gopass insert apps/alice-qa/rabbitmq_password # only if enable_rabbitmq = true
|
||||
gopass insert apps/alice-qa/loki_token # only if Loki requires auth
|
||||
```
|
||||
|
||||
#### Step 4 — (Optional) Create the app QA tfvars
|
||||
|
||||
If you want the chained deploy to fire, commit an app tfvars alongside the VM tfvars.
|
||||
Use any placeholder for `ssh_host` — the pipeline always overrides it with the actual VM IP.
|
||||
|
||||
```hcl
|
||||
# apps/alice-qa.tfvars (for vm_role = "docker")
|
||||
|
||||
ssh_host = "placeholder" # overridden by pipeline with actual VM IP
|
||||
ssh_user = "developer"
|
||||
|
||||
app_name = "alice-qa"
|
||||
environment = "dev"
|
||||
|
||||
openresty_source_type = "git_clone"
|
||||
openresty_git_repo = "https://gitea.example.com/alice/myapp-openresty.git"
|
||||
openresty_git_ref = "v0.1.0"
|
||||
openresty_external_port = 8080
|
||||
|
||||
db_name = "alice_qa"
|
||||
db_user = "alice"
|
||||
```
|
||||
|
||||
#### Step 5 — Commit and push to main
|
||||
|
||||
```sh
|
||||
git add proxmox/apps/alice.tfvars
|
||||
git add apps/alice-qa.tfvars # optional, for chained deploy
|
||||
git commit -m "chore: add developer VM for alice"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
#### What the pipeline does
|
||||
|
||||
```
|
||||
deploy-proxmox.yml
|
||||
│
|
||||
├── tofu apply (proxmox/)
|
||||
│ └── Proxmox VM created, cloud-init installs Docker or k3s
|
||||
│
|
||||
├── Read outputs: VM IP, vm_role, key_was_generated
|
||||
│
|
||||
├── If key_was_generated == true:
|
||||
│ └── gopass insert infra/ssh-keys/alice ← private key from Tofu state
|
||||
│
|
||||
├── If vm_role == "k3s":
|
||||
│ ├── SSH to VM, wait for k3s to be active (up to 5 minutes)
|
||||
│ ├── Fetch /etc/rancher/k3s/k3s.yaml
|
||||
│ ├── Replace 127.0.0.1 with actual VM IP
|
||||
│ └── gopass insert infra/kubeconfigs/alice
|
||||
│
|
||||
├── If apps/alice-qa.tfvars exists (vm_role = "docker"):
|
||||
│ └── tofu apply (root/) with -var="ssh_host=<VM IP>"
|
||||
│ └── Deploys OpenResty + PostgreSQL + Redis onto the new VM
|
||||
│
|
||||
└── If k8s/apps/alice-qa.tfvars exists (vm_role = "k3s"):
|
||||
└── tofu apply (k8s/) with retrieved kubeconfig
|
||||
└── Deploys Node-RED + optional RabbitMQ onto the new VM
|
||||
```
|
||||
|
||||
#### Accessing the VM after provisioning
|
||||
|
||||
The developer can SSH using:
|
||||
|
||||
```sh
|
||||
# If Tofu generated the key — retrieve it from gopass:
|
||||
gopass show -o infra/ssh-keys/alice > ~/.ssh/alice-vm && chmod 600 ~/.ssh/alice-vm
|
||||
ssh -i ~/.ssh/alice-vm developer@<VM IP>
|
||||
|
||||
# If the developer provided their own key:
|
||||
ssh developer@<VM IP>
|
||||
```
|
||||
|
||||
The VM IP is visible in the pipeline logs and in the Tofu state:
|
||||
|
||||
```sh
|
||||
cd proxmox
|
||||
tofu workspace select alice
|
||||
tofu output vm_ip
|
||||
```
|
||||
|
||||
### Chained App Deploy
|
||||
|
||||
When a `<devname>-qa.tfvars` file exists at commit time, the Proxmox pipeline
|
||||
automatically deploys the app onto the freshly provisioned VM in the same pipeline run.
|
||||
No separate trigger or manual step is required.
|
||||
|
||||
After initial provisioning, subsequent changes to the app tfvars (e.g. updating
|
||||
`openresty_git_ref`) are handled by the **regular Docker or K8s deploy pipeline**,
|
||||
not the Proxmox pipeline. The developer simply commits the updated app tfvars to `main`
|
||||
as they would for any production app.
|
||||
|
||||
**Naming convention summary:**
|
||||
|
||||
| VM tfvars | App tfvars (Docker) | App tfvars (K8s) |
|
||||
|-----------|---------------------|-----------------|
|
||||
| `proxmox/apps/alice.tfvars` | `apps/alice-qa.tfvars` | `k8s/apps/alice-qa.tfvars` |
|
||||
| `proxmox/apps/bob.tfvars` | `apps/bob-qa.tfvars` | `k8s/apps/bob-qa.tfvars` |
|
||||
|
||||
### Decommissioning a Developer VM
|
||||
|
||||
To tear down a developer's VM and remove all associated credentials:
|
||||
|
||||
```sh
|
||||
git rm proxmox/apps/alice.tfvars
|
||||
git commit -m "chore: decommission developer VM for alice"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
The `destroy` job in the pipeline will:
|
||||
|
||||
1. Run `tofu destroy` to delete the Proxmox VM
|
||||
2. Delete the Tofu workspace
|
||||
3. Remove `infra/ssh-keys/alice` from gopass
|
||||
4. Remove `infra/kubeconfigs/alice` from gopass (if it existed)
|
||||
|
||||
> **Note:** The app QA deployment (`apps/alice-qa.tfvars`) is **not** automatically
|
||||
> destroyed when the VM is decommissioned. Delete that file in a separate commit to
|
||||
> trigger the standard Docker/K8s destroy pipeline, or clean it up manually with
|
||||
> `tofu destroy` before removing the VM.
|
||||
|
||||
---
|
||||
|
||||
## CI/CD Pipelines
|
||||
|
||||
Both Gitea Actions and GitLab CI implement the same three pipelines:
|
||||
Both Gitea Actions and GitLab CI implement the same four 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 |
|
||||
| Proxmox deploy | `.gitea/workflows/deploy-proxmox.yml` | `.gitlab/workflows/deploy-proxmox.gitlab-ci.yml` | `main` + `proxmox/apps/*.tfvars` changed |
|
||||
|
||||
### Docker Pipeline
|
||||
|
||||
@@ -680,6 +1056,22 @@ Both Gitea Actions and GitLab CI implement the same three pipelines:
|
||||
6. `tofu apply` (working dir: `k8s/`) with tfvars + runtime secrets
|
||||
7. Cleanup: `rm /tmp/kubeconfig`
|
||||
|
||||
### Proxmox Pipeline
|
||||
|
||||
**Trigger:** push to `main` with changes to `proxmox/apps/*.tfvars`
|
||||
|
||||
**Steps per changed file:**
|
||||
|
||||
1. Resolve developer name from filename (`proxmox/apps/alice.tfvars` → `alice`)
|
||||
2. `tofu init` against SeaweedFS backend (state key: `apps-proxmox/alice.tfstate`)
|
||||
3. `tofu workspace select alice` or create if new
|
||||
4. `tofu apply` with the tfvars + Proxmox API credentials via `-var` flags
|
||||
5. Read VM outputs: IP, role, whether a key was generated
|
||||
6. If key was generated → store SSH private key in gopass `infra/ssh-keys/alice`
|
||||
7. If `vm_role = k3s` → SSH to VM, wait for k3s, fetch kubeconfig, store in gopass `infra/kubeconfigs/alice`
|
||||
8. If `apps/alice-qa.tfvars` exists (`vm_role = docker`) → run Docker `tofu apply` with VM IP override
|
||||
9. If `k8s/apps/alice-qa.tfvars` exists (`vm_role = k3s`) → run K8s `tofu apply` with retrieved kubeconfig
|
||||
|
||||
### Destroy on Delete
|
||||
|
||||
Both pipelines include a `destroy` job. When a `.tfvars` file is **deleted** from the
|
||||
@@ -816,9 +1208,10 @@ 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
|
||||
make test-unit # all stacks
|
||||
make test-unit-docker # Docker stack only
|
||||
make test-unit-k8s # K8s stack only
|
||||
make test-unit-proxmox # Proxmox stack only
|
||||
```
|
||||
|
||||
#### What is tested
|
||||
@@ -845,6 +1238,18 @@ make test-unit-k8s # K8s stack only
|
||||
| `output.namespace == var.app_name` | Assertion |
|
||||
| `output.app_url_path == var.app_path_prefix` | Assertion |
|
||||
|
||||
**Proxmox stack** ([proxmox/tests/proxmox_validation.tftest.hcl](proxmox/tests/proxmox_validation.tftest.hcl)):
|
||||
|
||||
| Test | Type |
|
||||
|------|------|
|
||||
| `vm_role = "docker"` plans and output is propagated | Smoke |
|
||||
| `vm_role = "k3s"` plans and output is propagated | Smoke |
|
||||
| `vm_role = "nomad"` is rejected | Validation |
|
||||
| `vm_role = "both"` is rejected | Validation |
|
||||
| `key_was_generated == true` when `ssh_public_key = ""` | Assertion |
|
||||
| `key_was_generated == false` when `ssh_public_key` is provided | Assertion |
|
||||
| Default `cloud_init_user` is `"developer"` | Assertion |
|
||||
|
||||
#### Running a single test
|
||||
|
||||
```sh
|
||||
@@ -931,10 +1336,10 @@ static ──► unit-docker ──►
|
||||
### Quick reference
|
||||
|
||||
```sh
|
||||
make test-all # L1 + L2 (fast, no infra needed)
|
||||
make test-integration-k8s # L3 K8s (needs k3d)
|
||||
make test-all # L1 + L2 (fast, no infra needed) — all three stacks
|
||||
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
|
||||
make clean # remove .terraform dirs and local state files
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
18
modules/vm-proxmox/cloud-init-docker.yaml.tftpl
Normal file
18
modules/vm-proxmox/cloud-init-docker.yaml.tftpl
Normal file
@@ -0,0 +1,18 @@
|
||||
#cloud-config
|
||||
# Installs Docker CE on first boot using the official convenience script.
|
||||
# The cloud-init user is added to the docker group for rootless CLI access.
|
||||
|
||||
package_update: true
|
||||
package_upgrade: false
|
||||
|
||||
packages:
|
||||
- curl
|
||||
- ca-certificates
|
||||
- gnupg
|
||||
|
||||
runcmd:
|
||||
- curl -fsSL https://get.docker.com | sh
|
||||
- usermod -aG docker ${cloud_init_user}
|
||||
- systemctl enable --now docker
|
||||
|
||||
final_message: "Docker installation complete. System ready after $UPTIME seconds."
|
||||
18
modules/vm-proxmox/cloud-init-k3s.yaml.tftpl
Normal file
18
modules/vm-proxmox/cloud-init-k3s.yaml.tftpl
Normal file
@@ -0,0 +1,18 @@
|
||||
#cloud-config
|
||||
# Installs k3s single-node on first boot using the official installer.
|
||||
# The kubeconfig at /etc/rancher/k3s/k3s.yaml is made world-readable so the
|
||||
# CI pipeline can retrieve it via SSH without requiring sudo.
|
||||
|
||||
package_update: true
|
||||
package_upgrade: false
|
||||
|
||||
packages:
|
||||
- curl
|
||||
|
||||
runcmd:
|
||||
- curl -sfL https://get.k3s.io | sh -
|
||||
- systemctl enable k3s
|
||||
- until systemctl is-active --quiet k3s; do sleep 3; done
|
||||
- chmod 644 /etc/rancher/k3s/k3s.yaml
|
||||
|
||||
final_message: "k3s installation complete. System ready after $UPTIME seconds."
|
||||
119
modules/vm-proxmox/main.tf
Normal file
119
modules/vm-proxmox/main.tf
Normal file
@@ -0,0 +1,119 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
proxmox = {
|
||||
source = "bpg/proxmox"
|
||||
version = ">= 0.66"
|
||||
}
|
||||
tls = {
|
||||
source = "hashicorp/tls"
|
||||
version = ">= 4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ─── SSH key handling ─────────────────────────────────────────────────────────
|
||||
# Generate an ED25519 key pair when the developer did not supply a public key.
|
||||
|
||||
resource "tls_private_key" "generated" {
|
||||
count = var.ssh_public_key == "" ? 1 : 0
|
||||
algorithm = "ED25519"
|
||||
}
|
||||
|
||||
locals {
|
||||
ssh_public_key = var.ssh_public_key != "" ? var.ssh_public_key : tls_private_key.generated[0].public_key_openssh
|
||||
ssh_private_key = var.ssh_public_key != "" ? null : tls_private_key.generated[0].private_key_openssh
|
||||
}
|
||||
|
||||
# ─── Cloud-init user-data ─────────────────────────────────────────────────────
|
||||
# Uploads a snippets file to Proxmox so cloud-init can install Docker or k3s
|
||||
# on first boot. The target datastore must have "Snippets" content enabled
|
||||
# (Proxmox UI → Datacenter → Storage → <store> → Content → Snippets).
|
||||
|
||||
resource "proxmox_virtual_environment_file" "cloud_init_config" {
|
||||
content_type = "snippets"
|
||||
datastore_id = var.snippets_datastore
|
||||
node_name = var.proxmox_node
|
||||
|
||||
source_raw {
|
||||
data = templatefile("${path.module}/cloud-init-${var.vm_role}.yaml.tftpl", {
|
||||
cloud_init_user = var.cloud_init_user
|
||||
})
|
||||
file_name = "${var.vm_name}-cloud-init.yaml"
|
||||
}
|
||||
}
|
||||
|
||||
# ─── Virtual machine ──────────────────────────────────────────────────────────
|
||||
|
||||
resource "proxmox_virtual_environment_vm" "dev_vm" {
|
||||
name = var.vm_name
|
||||
node_name = var.proxmox_node
|
||||
vm_id = var.vm_id > 0 ? var.vm_id : null
|
||||
|
||||
clone {
|
||||
vm_id = var.template_vm_id
|
||||
full = true
|
||||
}
|
||||
|
||||
# QEMU guest agent must be pre-installed in the template.
|
||||
# Required for Tofu to read the DHCP-assigned IP after boot.
|
||||
agent {
|
||||
enabled = true
|
||||
}
|
||||
|
||||
cpu {
|
||||
cores = var.cores
|
||||
type = "host"
|
||||
}
|
||||
|
||||
memory {
|
||||
dedicated = var.memory_mb
|
||||
}
|
||||
|
||||
disk {
|
||||
datastore_id = var.storage_pool
|
||||
size = var.disk_size_gb
|
||||
interface = "scsi0"
|
||||
file_format = "raw"
|
||||
discard = "on"
|
||||
iothread = true
|
||||
}
|
||||
|
||||
network_device {
|
||||
bridge = var.bridge
|
||||
vlan_id = var.vlan_tag
|
||||
}
|
||||
|
||||
initialization {
|
||||
ip_config {
|
||||
ipv4 {
|
||||
address = "dhcp"
|
||||
}
|
||||
}
|
||||
|
||||
user_account {
|
||||
username = var.cloud_init_user
|
||||
keys = [local.ssh_public_key]
|
||||
}
|
||||
|
||||
# Custom user-data installs Docker CE or k3s via cloud-init runcmd.
|
||||
user_data_file_id = proxmox_virtual_environment_file.cloud_init_config.id
|
||||
}
|
||||
|
||||
operating_system {
|
||||
type = "l26"
|
||||
}
|
||||
|
||||
# Wait up to 10 minutes for the VM to boot and the guest agent to report an IP.
|
||||
timeout_create = 600
|
||||
}
|
||||
|
||||
# ─── Derived locals ───────────────────────────────────────────────────────────
|
||||
|
||||
locals {
|
||||
# Collect all IPv4 addresses from non-loopback interfaces (reported by QEMU agent).
|
||||
vm_ips = flatten([
|
||||
for i, name in proxmox_virtual_environment_vm.dev_vm.network_interface_names :
|
||||
proxmox_virtual_environment_vm.dev_vm.ipv4_addresses[i]
|
||||
if name != "lo" && length(proxmox_virtual_environment_vm.dev_vm.ipv4_addresses[i]) > 0
|
||||
])
|
||||
}
|
||||
30
modules/vm-proxmox/outputs.tf
Normal file
30
modules/vm-proxmox/outputs.tf
Normal file
@@ -0,0 +1,30 @@
|
||||
output "vm_ip" {
|
||||
description = "Primary IPv4 address of the VM (DHCP-assigned, read via QEMU guest agent)."
|
||||
value = local.vm_ips[0]
|
||||
}
|
||||
|
||||
output "vm_id" {
|
||||
description = "Proxmox VM ID assigned to the virtual machine."
|
||||
value = proxmox_virtual_environment_vm.dev_vm.vm_id
|
||||
}
|
||||
|
||||
output "vm_role" {
|
||||
description = "Software role installed on the VM: 'docker' or 'k3s'."
|
||||
value = var.vm_role
|
||||
}
|
||||
|
||||
output "cloud_init_user" {
|
||||
description = "Username created on the VM by cloud-init."
|
||||
value = var.cloud_init_user
|
||||
}
|
||||
|
||||
output "ssh_private_key" {
|
||||
description = "Generated SSH private key in OpenSSH format. Empty string when the caller supplied their own ssh_public_key."
|
||||
value = local.ssh_private_key != null ? local.ssh_private_key : ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
output "key_was_generated" {
|
||||
description = "True when Tofu generated the SSH key pair; false when the caller provided ssh_public_key."
|
||||
value = var.ssh_public_key == ""
|
||||
}
|
||||
90
modules/vm-proxmox/variables.tf
Normal file
90
modules/vm-proxmox/variables.tf
Normal file
@@ -0,0 +1,90 @@
|
||||
# ─── Proxmox placement ────────────────────────────────────────────────────────
|
||||
|
||||
variable "proxmox_node" {
|
||||
description = "Proxmox cluster node name where the VM will be created."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "vm_name" {
|
||||
description = "VM hostname and Proxmox display name. Derived from the Tofu workspace (developer name)."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "vm_id" {
|
||||
description = "Explicit Proxmox VM ID. Set to 0 to let Proxmox auto-assign."
|
||||
type = number
|
||||
default = 0
|
||||
}
|
||||
|
||||
variable "template_vm_id" {
|
||||
description = "VM ID of the cloud-init template to clone. Must have qemu-guest-agent pre-installed."
|
||||
type = number
|
||||
}
|
||||
|
||||
variable "storage_pool" {
|
||||
description = "Proxmox storage pool for the VM disk (e.g. 'local-lvm', 'ceph-pool')."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "snippets_datastore" {
|
||||
description = "Proxmox datastore with 'snippets' content enabled, used to upload cloud-init user-data."
|
||||
type = string
|
||||
default = "local"
|
||||
}
|
||||
|
||||
# ─── VM sizing ────────────────────────────────────────────────────────────────
|
||||
|
||||
variable "cores" {
|
||||
description = "Number of vCPU cores."
|
||||
type = number
|
||||
default = 2
|
||||
}
|
||||
|
||||
variable "memory_mb" {
|
||||
description = "RAM in megabytes."
|
||||
type = number
|
||||
default = 2048
|
||||
}
|
||||
|
||||
variable "disk_size_gb" {
|
||||
description = "Root disk size in gigabytes."
|
||||
type = number
|
||||
default = 20
|
||||
}
|
||||
|
||||
# ─── Networking ───────────────────────────────────────────────────────────────
|
||||
|
||||
variable "bridge" {
|
||||
description = "Proxmox network bridge (e.g. 'vmbr0', 'vmbr1')."
|
||||
type = string
|
||||
default = "vmbr0"
|
||||
}
|
||||
|
||||
variable "vlan_tag" {
|
||||
description = "Optional VLAN tag for the VM NIC. null = untagged/native VLAN."
|
||||
type = number
|
||||
default = null
|
||||
}
|
||||
|
||||
# ─── VM role & access ─────────────────────────────────────────────────────────
|
||||
|
||||
variable "vm_role" {
|
||||
description = "Software to install on the VM: 'docker' installs Docker CE, 'k3s' installs k3s single-node."
|
||||
type = string
|
||||
validation {
|
||||
condition = contains(["docker", "k3s"], var.vm_role)
|
||||
error_message = "vm_role must be 'docker' or 'k3s'."
|
||||
}
|
||||
}
|
||||
|
||||
variable "cloud_init_user" {
|
||||
description = "Username to create via cloud-init on the VM."
|
||||
type = string
|
||||
default = "developer"
|
||||
}
|
||||
|
||||
variable "ssh_public_key" {
|
||||
description = "Developer's SSH public key to inject. Leave empty to auto-generate an ED25519 key pair (private key stored in Tofu state, exposed as a sensitive output)."
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
49
proxmox/apps/example-developer.tfvars
Normal file
49
proxmox/apps/example-developer.tfvars
Normal file
@@ -0,0 +1,49 @@
|
||||
# ─── Example developer VM configuration ──────────────────────────────────────
|
||||
#
|
||||
# File naming convention: proxmox/apps/<devname>.tfvars
|
||||
# The filename becomes the Tofu workspace name, the VM hostname, and the
|
||||
# key name used for gopass secrets (infra/ssh-keys/<devname>).
|
||||
#
|
||||
# Required Gitea secrets (set once in the repository settings):
|
||||
# PROXMOX_ENDPOINT — https://proxmox.example.com:8006/
|
||||
# PROXMOX_API_TOKEN — user@pam!tokenid=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||
# PROXMOX_TLS_INSECURE — true (if using a self-signed cert), otherwise omit
|
||||
#
|
||||
# Chained deploy convention:
|
||||
# vm_role = "docker" → also applies apps/<devname>-qa.tfvars (if present)
|
||||
# vm_role = "k3s" → also applies k8s/apps/<devname>-qa.tfvars (if present)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
# ─── Proxmox placement ───────────────────────────────────────────────────────
|
||||
|
||||
proxmox_node = "pve" # your Proxmox node name
|
||||
template_vm_id = 9000 # VM ID of your cloud-init template (must have qemu-guest-agent)
|
||||
storage_pool = "local-lvm"
|
||||
|
||||
# snippets_datastore = "local" # default; must have Snippets content type enabled
|
||||
|
||||
# ─── VM sizing ───────────────────────────────────────────────────────────────
|
||||
|
||||
cores = 2
|
||||
memory_mb = 2048
|
||||
disk_size_gb = 30
|
||||
|
||||
# ─── Networking ──────────────────────────────────────────────────────────────
|
||||
|
||||
bridge = "vmbr0"
|
||||
|
||||
# vlan_tag = 100 # uncomment to place the VM in a specific VLAN
|
||||
|
||||
# ─── Role & access ───────────────────────────────────────────────────────────
|
||||
|
||||
# "docker" → installs Docker CE; pairs with apps/<devname>-qa.tfvars
|
||||
# "k3s" → installs k3s; pairs with k8s/apps/<devname>-qa.tfvars
|
||||
vm_role = "docker"
|
||||
|
||||
cloud_init_user = "developer"
|
||||
|
||||
# Leave empty to auto-generate an ED25519 key pair.
|
||||
# The private key is stored in Tofu state and automatically saved to gopass
|
||||
# as infra/ssh-keys/<devname> by the CI pipeline.
|
||||
# Or paste the developer's SSH public key here:
|
||||
ssh_public_key = ""
|
||||
8
proxmox/backend.tf
Normal file
8
proxmox/backend.tf
Normal file
@@ -0,0 +1,8 @@
|
||||
terraform {
|
||||
# Local backend is the default — safe for local testing and tofu validate.
|
||||
# Uncomment the block below and run `tofu init` with backend-config flags
|
||||
# (or scripts/setup-backend.sh) to switch to SeaweedFS S3.
|
||||
# State key pattern: apps-proxmox/<devname>.tfstate
|
||||
|
||||
# backend "s3" {}
|
||||
}
|
||||
70
proxmox/main.tf
Normal file
70
proxmox/main.tf
Normal file
@@ -0,0 +1,70 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
proxmox = {
|
||||
source = "bpg/proxmox"
|
||||
version = ">= 0.66"
|
||||
}
|
||||
tls = {
|
||||
source = "hashicorp/tls"
|
||||
version = ">= 4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "proxmox" {
|
||||
endpoint = var.proxmox_endpoint
|
||||
api_token = var.proxmox_api_token
|
||||
insecure = var.proxmox_tls_insecure
|
||||
}
|
||||
|
||||
# The Tofu workspace name becomes the VM name and developer identifier.
|
||||
# One workspace = one developer VM = one proxmox/apps/<devname>.tfvars file.
|
||||
module "dev_vm" {
|
||||
source = "../modules/vm-proxmox"
|
||||
|
||||
proxmox_node = var.proxmox_node
|
||||
vm_name = terraform.workspace
|
||||
vm_id = var.vm_id
|
||||
template_vm_id = var.template_vm_id
|
||||
storage_pool = var.storage_pool
|
||||
snippets_datastore = var.snippets_datastore
|
||||
cores = var.cores
|
||||
memory_mb = var.memory_mb
|
||||
disk_size_gb = var.disk_size_gb
|
||||
bridge = var.bridge
|
||||
vlan_tag = var.vlan_tag
|
||||
vm_role = var.vm_role
|
||||
cloud_init_user = var.cloud_init_user
|
||||
ssh_public_key = var.ssh_public_key
|
||||
}
|
||||
|
||||
output "vm_ip" {
|
||||
description = "VM IPv4 address (DHCP-assigned, read via QEMU guest agent)."
|
||||
value = module.dev_vm.vm_ip
|
||||
}
|
||||
|
||||
output "vm_id" {
|
||||
description = "Proxmox VM ID."
|
||||
value = module.dev_vm.vm_id
|
||||
}
|
||||
|
||||
output "vm_role" {
|
||||
description = "Software role installed: 'docker' or 'k3s'."
|
||||
value = module.dev_vm.vm_role
|
||||
}
|
||||
|
||||
output "cloud_init_user" {
|
||||
description = "Username on the VM."
|
||||
value = module.dev_vm.cloud_init_user
|
||||
}
|
||||
|
||||
output "ssh_private_key" {
|
||||
description = "Generated SSH private key (empty string when caller provided ssh_public_key)."
|
||||
value = module.dev_vm.ssh_private_key
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
output "key_was_generated" {
|
||||
description = "True when Tofu generated the SSH key pair."
|
||||
value = module.dev_vm.key_was_generated
|
||||
}
|
||||
101
proxmox/tests/proxmox_validation.tftest.hcl
Normal file
101
proxmox/tests/proxmox_validation.tftest.hcl
Normal file
@@ -0,0 +1,101 @@
|
||||
# Unit tests for the Proxmox stack using mock providers.
|
||||
# No real Proxmox or TLS infrastructure is required.
|
||||
|
||||
mock_provider "proxmox" {}
|
||||
mock_provider "tls" {}
|
||||
|
||||
# ─── Shared variable defaults ─────────────────────────────────────────────────
|
||||
|
||||
variables {
|
||||
proxmox_endpoint = "https://proxmox.example.com:8006/"
|
||||
proxmox_api_token = "test@pam!ci=00000000-0000-0000-0000-000000000000"
|
||||
proxmox_node = "pve"
|
||||
template_vm_id = 9000
|
||||
storage_pool = "local-lvm"
|
||||
vm_role = "docker"
|
||||
}
|
||||
|
||||
# ─── Role: docker ─────────────────────────────────────────────────────────────
|
||||
|
||||
run "docker_vm_role_is_propagated" {
|
||||
command = plan
|
||||
|
||||
assert {
|
||||
condition = module.dev_vm.vm_role == "docker"
|
||||
error_message = "vm_role output should equal the input 'docker'."
|
||||
}
|
||||
}
|
||||
|
||||
run "docker_vm_key_generated_when_no_public_key_provided" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
ssh_public_key = ""
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = module.dev_vm.key_was_generated == true
|
||||
error_message = "key_was_generated should be true when ssh_public_key is empty."
|
||||
}
|
||||
}
|
||||
|
||||
run "docker_vm_key_not_generated_when_public_key_provided" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
ssh_public_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExampleKeyForTesting developer@example.com"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = module.dev_vm.key_was_generated == false
|
||||
error_message = "key_was_generated should be false when ssh_public_key is provided."
|
||||
}
|
||||
}
|
||||
|
||||
# ─── Role: k3s ────────────────────────────────────────────────────────────────
|
||||
|
||||
run "k3s_vm_role_is_propagated" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
vm_role = "k3s"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = module.dev_vm.vm_role == "k3s"
|
||||
error_message = "vm_role output should equal the input 'k3s'."
|
||||
}
|
||||
}
|
||||
|
||||
# ─── Variable validation ──────────────────────────────────────────────────────
|
||||
|
||||
run "reject_invalid_vm_role" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
vm_role = "nomad"
|
||||
}
|
||||
|
||||
expect_failures = [var.vm_role]
|
||||
}
|
||||
|
||||
run "reject_another_invalid_role" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
vm_role = "both"
|
||||
}
|
||||
|
||||
expect_failures = [var.vm_role]
|
||||
}
|
||||
|
||||
# ─── Sizing defaults are applied ──────────────────────────────────────────────
|
||||
|
||||
run "default_cloud_init_user" {
|
||||
command = plan
|
||||
|
||||
assert {
|
||||
condition = module.dev_vm.cloud_init_user == "developer"
|
||||
error_message = "Default cloud_init_user should be 'developer'."
|
||||
}
|
||||
}
|
||||
104
proxmox/variables.tf
Normal file
104
proxmox/variables.tf
Normal file
@@ -0,0 +1,104 @@
|
||||
# ─── Proxmox connection (injected at CI runtime from Gitea secrets) ───────────
|
||||
|
||||
variable "proxmox_endpoint" {
|
||||
description = "Proxmox API endpoint URL, e.g. 'https://proxmox.example.com:8006/'."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "proxmox_api_token" {
|
||||
description = "Proxmox API token in 'user@realm!tokenid=secret' format."
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "proxmox_tls_insecure" {
|
||||
description = "Skip TLS verification for the Proxmox API. Set to true when using a self-signed certificate."
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
# ─── Proxmox placement ────────────────────────────────────────────────────────
|
||||
|
||||
variable "proxmox_node" {
|
||||
description = "Proxmox cluster node name where the VM will be created."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "template_vm_id" {
|
||||
description = "VM ID of the cloud-init-enabled template to clone. Must have qemu-guest-agent pre-installed."
|
||||
type = number
|
||||
}
|
||||
|
||||
variable "storage_pool" {
|
||||
description = "Proxmox storage pool for the VM disk (e.g. 'local-lvm', 'ceph-pool')."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "snippets_datastore" {
|
||||
description = "Proxmox datastore with 'Snippets' content enabled, used for cloud-init user-data upload."
|
||||
type = string
|
||||
default = "local"
|
||||
}
|
||||
|
||||
# ─── VM sizing ────────────────────────────────────────────────────────────────
|
||||
|
||||
variable "vm_id" {
|
||||
description = "Explicit Proxmox VM ID. Set to 0 for Proxmox to auto-assign."
|
||||
type = number
|
||||
default = 0
|
||||
}
|
||||
|
||||
variable "cores" {
|
||||
description = "Number of vCPU cores."
|
||||
type = number
|
||||
default = 2
|
||||
}
|
||||
|
||||
variable "memory_mb" {
|
||||
description = "RAM in megabytes."
|
||||
type = number
|
||||
default = 2048
|
||||
}
|
||||
|
||||
variable "disk_size_gb" {
|
||||
description = "Root disk size in gigabytes."
|
||||
type = number
|
||||
default = 20
|
||||
}
|
||||
|
||||
# ─── Networking ───────────────────────────────────────────────────────────────
|
||||
|
||||
variable "bridge" {
|
||||
description = "Proxmox network bridge (e.g. 'vmbr0', 'vmbr1')."
|
||||
type = string
|
||||
default = "vmbr0"
|
||||
}
|
||||
|
||||
variable "vlan_tag" {
|
||||
description = "Optional VLAN tag for the VM NIC. null = untagged/native VLAN."
|
||||
type = number
|
||||
default = null
|
||||
}
|
||||
|
||||
# ─── VM role & access ─────────────────────────────────────────────────────────
|
||||
|
||||
variable "vm_role" {
|
||||
description = "Software to install on the VM: 'docker' installs Docker CE, 'k3s' installs k3s single-node."
|
||||
type = string
|
||||
validation {
|
||||
condition = contains(["docker", "k3s"], var.vm_role)
|
||||
error_message = "vm_role must be 'docker' or 'k3s'."
|
||||
}
|
||||
}
|
||||
|
||||
variable "cloud_init_user" {
|
||||
description = "Username to create via cloud-init on the VM."
|
||||
type = string
|
||||
default = "developer"
|
||||
}
|
||||
|
||||
variable "ssh_public_key" {
|
||||
description = "Developer's SSH public key to inject into the VM. Leave empty to auto-generate an ED25519 key pair."
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
Reference in New Issue
Block a user