stages: - validate - plan - gitops - approve - apply - compliance - reconcile variables: TF_ROOT: ${CI_PROJECT_DIR} TF_STATE_NAME: cloudflare-infra TF_PLAN_FILE: tfplan.binary TF_PLAN_JSON: tfplan.json cache: key: ${CI_COMMIT_REF_SLUG} paths: - ${TF_ROOT}/.terraform .terraform_base: image: hashicorp/terraform:1.6 before_script: - cd ${TF_ROOT} - terraform init -input=false # Stage 1: Validate terraform_fmt: extends: .terraform_base stage: validate script: - terraform fmt -check -recursive allow_failure: false terraform_validate: extends: .terraform_base stage: validate script: - terraform validate allow_failure: false # Stage 2: Plan terraform_plan: extends: .terraform_base stage: plan script: - terraform plan -out=${TF_PLAN_FILE} -input=false - terraform show -json ${TF_PLAN_FILE} > ${TF_PLAN_JSON} artifacts: name: "terraform-plan-${CI_COMMIT_SHORT_SHA}" paths: - ${TF_ROOT}/${TF_PLAN_FILE} - ${TF_ROOT}/${TF_PLAN_JSON} expire_in: 7 days rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # Stage 3: Manual Approval Gate manual_approval: stage: approve script: - echo "Terraform plan approved by ${GITLAB_USER_NAME}" when: manual allow_failure: false rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH needs: - terraform_plan # Stage 4: Apply terraform_apply: extends: .terraform_base stage: apply script: - terraform apply -input=false ${TF_PLAN_FILE} - terraform output -json > terraform_outputs.json artifacts: name: "terraform-outputs-${CI_COMMIT_SHORT_SHA}" paths: - ${TF_ROOT}/terraform_outputs.json expire_in: 30 days rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH needs: - manual_approval environment: name: production action: start # Stage 5: Compliance compliance_report: stage: compliance image: python:3.11-slim before_script: - pip install blake3 script: - | # Generate compliance snapshot TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ) COMMIT_SHA=${CI_COMMIT_SHA} # Hash all terraform files find ${TF_ROOT} -name "*.tf" -exec cat {} \; | python3 -c " import sys import blake3 import json content = sys.stdin.read() tf_hash = blake3.blake3(content.encode()).hexdigest() receipt = { 'receipt_type': 'terraform_compliance', 'schema_version': 'vm_tf_compliance_v1', 'timestamp': '${TIMESTAMP}', 'commit_sha': '${COMMIT_SHA}', 'pipeline_id': '${CI_PIPELINE_ID}', 'job_id': '${CI_JOB_ID}', 'tf_files_hash': tf_hash, 'applied_by': '${GITLAB_USER_NAME}', 'environment': 'production' } print(json.dumps(receipt, indent=2)) " > compliance_receipt.json cat compliance_receipt.json artifacts: name: "compliance-${CI_COMMIT_SHORT_SHA}" paths: - compliance_receipt.json - ${TF_ROOT}/terraform_outputs.json expire_in: 365 days rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH needs: - terraform_apply # Merge Request: Plan Only mr_plan: extends: .terraform_base stage: plan script: - terraform plan -input=false -no-color -out=plan.tfplan | tee plan_output.txt - terraform show -json plan.tfplan > plan.json artifacts: paths: - ${TF_ROOT}/plan_output.txt - ${TF_ROOT}/plan.tfplan - ${TF_ROOT}/plan.json expire_in: 7 days rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" # ============================================================================== # PHASE 6 - GITOPS PR WORKFLOWS # ============================================================================== # Post plan summary as MR comment gitops:plan_comment: stage: gitops image: python:3.12-slim before_script: - pip install requests pyyaml script: - | cd ${CI_PROJECT_DIR}/gitops python3 ci_plan_comment.py variables: GITLAB_TOKEN: ${GITLAB_TOKEN} artifacts: paths: - plan_output.env reports: dotenv: plan_output.env expire_in: 1 day rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" needs: - mr_plan # Drift remediation (scheduled or alert-triggered) gitops:drift_remediation: stage: gitops image: python:3.12-slim before_script: - pip install requests pyyaml - apt-get update && apt-get install -y git - git config --global user.email "gitops-bot@cloudflare-mesh.local" - git config --global user.name "GitOps Bot" script: - | cd ${CI_PROJECT_DIR}/gitops python3 drift_pr_bot.py \ --trigger-source "${GITOPS_TRIGGER_SOURCE:-scheduled}" variables: GITLAB_TOKEN: ${GITLAB_TOKEN} GITOPS_DRY_RUN: "false" rules: # Scheduled runs - if: $CI_PIPELINE_SOURCE == "schedule" && $GITOPS_DRIFT_CHECK == "true" # Alert-triggered runs - if: $CI_PIPELINE_SOURCE == "trigger" && $GITOPS_TRIGGER_SOURCE == "alert" needs: [] # Risk gate - block high-risk changes without approval gitops:risk_gate: stage: gitops image: python:3.12-slim before_script: - pip install pyyaml script: - | cd ${CI_PROJECT_DIR}/gitops RISK=$(python3 plan_summarizer.py --format json | python3 -c "import sys,json; print(json.load(sys.stdin)['overall_risk'])") echo "Overall risk level: $RISK" if [ "$RISK" = "CRITICAL" ]; then echo "CRITICAL risk detected. Manual approval required." exit 1 fi echo "Risk level acceptable for auto-merge consideration." rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" needs: - mr_plan allow_failure: true # Stage 6: Deep Binding - State Reconciliation state_reconcile: stage: reconcile image: python:3.11-slim variables: SCRIPTS_DIR: ${CI_PROJECT_DIR}/../scripts before_script: - pip install requests script: - | echo "=== Cloudflare State Reconciliation ===" # Run state reconciler python3 ${CI_PROJECT_DIR}/../scripts/state-reconciler.py \ --zone-id ${CLOUDFLARE_ZONE_ID} \ --account-id ${CLOUDFLARE_ACCOUNT_ID} \ --output-dir ${CI_PROJECT_DIR}/../snapshots \ --receipt-dir ${CI_PROJECT_DIR}/../receipts # Find latest snapshot SNAPSHOT=$(ls -t ${CI_PROJECT_DIR}/../snapshots/cloudflare-*.json | head -1) echo "Snapshot: $SNAPSHOT" # Run invariant checker python3 ${CI_PROJECT_DIR}/../scripts/invariant-checker.py \ --snapshot "$SNAPSHOT" \ --output-dir ${CI_PROJECT_DIR}/../anomalies || INVARIANT_FAILED=1 # Find latest report REPORT=$(ls -t ${CI_PROJECT_DIR}/../anomalies/invariant-report-*.json | head -1) echo "Report: $REPORT" # Copy artifacts mkdir -p reconcile_artifacts cp "$SNAPSHOT" reconcile_artifacts/ 2>/dev/null || true cp "$REPORT" reconcile_artifacts/ 2>/dev/null || true cp ${CI_PROJECT_DIR}/../anomalies/anomaly-*.json reconcile_artifacts/ 2>/dev/null || true # Summary python3 -c " import json with open('$REPORT') as f: r = json.load(f) print(f\"Passed: {r['summary']['passed']}\") print(f\"Failed: {r['summary']['failed']}\") " if [ "${INVARIANT_FAILED:-0}" = "1" ]; then echo "WARNING: Invariant failures detected" exit 1 fi artifacts: name: "reconcile-${CI_COMMIT_SHORT_SHA}" paths: - reconcile_artifacts/ expire_in: 365 days when: always rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH needs: - compliance_report allow_failure: true # Scheduled Reconciliation (Daily) scheduled_reconcile: extends: state_reconcile rules: - if: $CI_PIPELINE_SOURCE == "schedule" needs: [] # Monthly Tunnel Rotation monthly_rotation: stage: reconcile image: python:3.11-slim before_script: - pip install requests script: - | echo "=== Monthly Tunnel Rotation ===" python3 ${CI_PROJECT_DIR}/../scripts/tunnel-rotation-scheduler.py \ --account-id ${CLOUDFLARE_ACCOUNT_ID} \ --zone-id ${CLOUDFLARE_ZONE_ID} \ --max-age 90 \ --output-dir ${CI_PROJECT_DIR}/../receipts artifacts: name: "rotation-${CI_COMMIT_SHORT_SHA}" paths: - receipts/tunnel-rotation-*.json expire_in: 365 days rules: - if: $CI_PIPELINE_SOURCE == "schedule" && $ROTATION_CYCLE == "monthly" needs: [] # ProofChain Anchor (Post-Apply) proofchain_anchor: stage: reconcile image: python:3.11-slim before_script: - pip install requests script: - | echo "=== ProofChain Anchoring ===" # Run full anchor workflow bash ${CI_PROJECT_DIR}/../scripts/anchor-cloudflare-state.sh \ --zone-id ${CLOUDFLARE_ZONE_ID} \ --account-id ${CLOUDFLARE_ACCOUNT_ID} # Copy artifacts mkdir -p anchor_artifacts cp ${CI_PROJECT_DIR}/../snapshots/*.json anchor_artifacts/ 2>/dev/null || true cp ${CI_PROJECT_DIR}/../receipts/*.json anchor_artifacts/ 2>/dev/null || true cp ${CI_PROJECT_DIR}/../anomalies/*.json anchor_artifacts/ 2>/dev/null || true cp ${CI_PROJECT_DIR}/../proofchain-anchors.jsonl anchor_artifacts/ 2>/dev/null || true echo "Anchoring complete" artifacts: name: "anchor-${CI_COMMIT_SHORT_SHA}" paths: - anchor_artifacts/ expire_in: 365 days rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH needs: - terraform_apply allow_failure: true