diff --git a/.gitignore b/.gitignore index 2c8d239..4a25704 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ scenarios/*/ui/dist/ # Go /cmd/sample-orders/sample-orders dist/ +.worktrees/ diff --git a/scenarios.json b/scenarios.json index 136ac6f..4118e28 100644 --- a/scenarios.json +++ b/scenarios.json @@ -1,5 +1,5 @@ { - "lastUpdated": "2026-02-26T08:57:15.226083Z", + "lastUpdated": "2026-03-09T04:49:56.855342Z", "componentVersions": { "workflow": "v0.2.15", "workflow-cloud": "v0.1.0", @@ -624,6 +624,15 @@ "passCount": 0, "failCount": 0, "notes": "Exercises goakt actor model: actor.system, actor.pool (auto-managed + permanent), step.actor_ask, step.actor_send. Order lifecycle with stateful actors + round-robin worker pool." + }, + "51-bmw-iac": { + "status": "passing", + "namespace": "wf-scenario-51", + "deployed": true, + "testCount": 13, + "passCount": 13, + "failCount": 0, + "notes": "BMW IaC lifecycle with DO mock providers. Tests plan/apply/status/drift/destroy + idempotency." } } -} +} \ No newline at end of file diff --git a/scenarios/51-bmw-iac/config/app.yaml b/scenarios/51-bmw-iac/config/app.yaml new file mode 100644 index 0000000..d41073e --- /dev/null +++ b/scenarios/51-bmw-iac/config/app.yaml @@ -0,0 +1,366 @@ +modules: + - name: server + type: http.server + config: + address: ":8080" + + - name: router + type: http.router + dependsOn: [server] + + - name: cloud-mock + type: cloud.account + config: + provider: mock + region: nyc1 + + - name: infra-state + type: iac.state + config: + backend: filesystem + directory: /var/lib/workflow/iac-state + + - name: bmw-database + type: platform.do_database + config: + provider: mock + engine: pg + version: "16" + size: db-s-1vcpu-1gb + region: nyc1 + num_nodes: 1 + name: bmw-db + + - name: bmw-networking + type: platform.do_networking + config: + provider: mock + vpc: + name: bmw-vpc + region: nyc1 + ip_range: 10.10.10.0/24 + firewalls: + - name: bmw-firewall + inbound: + - protocol: tcp + ports: "443" + sources: ["0.0.0.0/0"] + + - name: bmw-app + type: platform.do_app + config: + provider: mock + name: buymywishlist + region: nyc + image: ghcr.io/gocodealone/buymywishlist:latest + instances: 1 + http_port: 8080 + envs: + DATABASE_URL: "mock://db" + JWT_SECRET: "mock-secret" + + - name: bmw-dns + type: platform.do_dns + config: + provider: mock + domain: buymywishlist.com + records: + - name: "@" + type: A + data: "127.0.0.1" + ttl: 300 + +workflows: + http: + router: router + server: server + routes: [] + +pipelines: + healthz: + trigger: + type: http + config: + path: /healthz + method: GET + steps: + - name: respond + type: step.json_response + config: + status: 200 + body: + status: ok + scenario: "51-bmw-iac" + + # Plan all resources + iac-plan-db: + trigger: + type: http + config: + path: /api/v1/iac/plan/database + method: POST + steps: + - name: plan + type: step.iac_plan + config: + platform: bmw-database + resource_id: bmw-database + state_store: infra-state + - name: respond + type: step.json_response + config: + status: 200 + body_from: "." + + iac-plan-net: + trigger: + type: http + config: + path: /api/v1/iac/plan/networking + method: POST + steps: + - name: plan + type: step.iac_plan + config: + platform: bmw-networking.iac + resource_id: bmw-networking + state_store: infra-state + - name: respond + type: step.json_response + config: + status: 200 + body_from: "." + + iac-plan-app: + trigger: + type: http + config: + path: /api/v1/iac/plan/app + method: POST + steps: + - name: plan + type: step.iac_plan + config: + platform: bmw-app.iac + resource_id: bmw-app + state_store: infra-state + - name: respond + type: step.json_response + config: + status: 200 + body_from: "." + + iac-plan-dns: + trigger: + type: http + config: + path: /api/v1/iac/plan/dns + method: POST + steps: + - name: plan + type: step.iac_plan + config: + platform: bmw-dns.iac + resource_id: bmw-dns + state_store: infra-state + - name: respond + type: step.json_response + config: + status: 200 + body_from: "." + + # Apply all resources + iac-apply-db: + trigger: + type: http + config: + path: /api/v1/iac/apply/database + method: POST + steps: + - name: apply + type: step.iac_apply + config: + platform: bmw-database + resource_id: bmw-database + state_store: infra-state + - name: respond + type: step.json_response + config: + status: 200 + body_from: "." + + iac-apply-net: + trigger: + type: http + config: + path: /api/v1/iac/apply/networking + method: POST + steps: + - name: apply + type: step.iac_apply + config: + platform: bmw-networking.iac + resource_id: bmw-networking + state_store: infra-state + - name: respond + type: step.json_response + config: + status: 200 + body_from: "." + + iac-apply-app: + trigger: + type: http + config: + path: /api/v1/iac/apply/app + method: POST + steps: + - name: apply + type: step.iac_apply + config: + platform: bmw-app.iac + resource_id: bmw-app + state_store: infra-state + - name: respond + type: step.json_response + config: + status: 200 + body_from: "." + + iac-apply-dns: + trigger: + type: http + config: + path: /api/v1/iac/apply/dns + method: POST + steps: + - name: apply + type: step.iac_apply + config: + platform: bmw-dns.iac + resource_id: bmw-dns + state_store: infra-state + - name: respond + type: step.json_response + config: + status: 200 + body_from: "." + + # Status all resources + iac-status-all: + trigger: + type: http + config: + path: /api/v1/iac/status + method: GET + steps: + - name: status-db + type: step.iac_status + config: + platform: bmw-database + resource_id: bmw-database + state_store: infra-state + - name: status-net + type: step.iac_status + config: + platform: bmw-networking.iac + resource_id: bmw-networking + state_store: infra-state + - name: status-app + type: step.iac_status + config: + platform: bmw-app.iac + resource_id: bmw-app + state_store: infra-state + - name: status-dns + type: step.iac_status + config: + platform: bmw-dns.iac + resource_id: bmw-dns + state_store: infra-state + - name: respond + type: step.json_response + config: + status: 200 + body_from: "." + + # Drift detection + iac-drift-db: + trigger: + type: http + config: + path: /api/v1/iac/drift/database + method: POST + steps: + - name: drift + type: step.iac_drift_detect + config: + platform: bmw-database + resource_id: bmw-database + state_store: infra-state + config: + engine: pg + version: "17" + - name: respond + type: step.json_response + config: + status: 200 + body_from: "." + + # Destroy all resources (reverse order) + iac-destroy-all: + trigger: + type: http + config: + path: /api/v1/iac + method: DELETE + steps: + - name: destroy-dns + type: step.iac_destroy + config: + platform: bmw-dns.iac + resource_id: bmw-dns + state_store: infra-state + - name: destroy-app + type: step.iac_destroy + config: + platform: bmw-app.iac + resource_id: bmw-app + state_store: infra-state + - name: destroy-net + type: step.iac_destroy + config: + platform: bmw-networking.iac + resource_id: bmw-networking + state_store: infra-state + - name: destroy-db + type: step.iac_destroy + config: + platform: bmw-database + resource_id: bmw-database + state_store: infra-state + - name: respond + type: step.json_response + config: + status: 200 + body_from: "." + + # Idempotency test — apply after apply should be no-op + iac-apply-idempotent: + trigger: + type: http + config: + path: /api/v1/iac/apply/idempotent + method: POST + steps: + - name: apply-db-again + type: step.iac_apply + config: + platform: bmw-database + resource_id: bmw-database + state_store: infra-state + - name: respond + type: step.json_response + config: + status: 200 + body_from: "." diff --git a/scenarios/51-bmw-iac/k8s/app.yaml b/scenarios/51-bmw-iac/k8s/app.yaml new file mode 100644 index 0000000..1039763 --- /dev/null +++ b/scenarios/51-bmw-iac/k8s/app.yaml @@ -0,0 +1,80 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: wf-scenario-51 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: app + namespace: wf-scenario-51 +data: + app.yaml: | + # Mounted from scenarios/51-bmw-iac/config/app.yaml +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: workflow + namespace: wf-scenario-51 +spec: + replicas: 1 + selector: + matchLabels: + app: workflow + template: + metadata: + labels: + app: workflow + spec: + containers: + - name: workflow + image: workflow-server:local + args: ["-config", "/config/app.yaml", "-data-dir", "/data"] + ports: + - containerPort: 8080 + volumeMounts: + - name: config + mountPath: /config + - name: data + mountPath: /data + - name: iac-state + mountPath: /var/lib/workflow/iac-state + readinessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 5 + volumes: + - name: config + configMap: + name: app + - name: data + emptyDir: {} + - name: iac-state + persistentVolumeClaim: + claimName: iac-state +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: iac-state + namespace: wf-scenario-51 +spec: + accessModes: [ReadWriteOnce] + resources: + requests: + storage: 100Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: workflow + namespace: wf-scenario-51 +spec: + selector: + app: workflow + ports: + - port: 8080 + targetPort: 8080 diff --git a/scenarios/51-bmw-iac/scenario.yaml b/scenarios/51-bmw-iac/scenario.yaml new file mode 100644 index 0000000..8124e7c --- /dev/null +++ b/scenarios/51-bmw-iac/scenario.yaml @@ -0,0 +1,16 @@ +name: BMW IaC (DigitalOcean Mock) +id: "51-bmw-iac" +category: C +description: | + Validates the full IaC lifecycle for BuyMyWishlist infrastructure on + DigitalOcean using mock providers. Exercises: + plan → apply → status → drift detect → destroy + across 4 resource types: managed database, VPC/firewall, App Platform, DNS. + State is persisted via the iac.state filesystem backend. + + All platform modules use mock backends — no real cloud API calls. +components: + - workflow (engine) + - cloud plugin (cloud.account) + - platform plugin (platform.do_database, platform.do_networking, platform.do_app, platform.do_dns, iac.state) +status: testable diff --git a/scenarios/51-bmw-iac/test/run.sh b/scenarios/51-bmw-iac/test/run.sh new file mode 100755 index 0000000..7f54f74 --- /dev/null +++ b/scenarios/51-bmw-iac/test/run.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +set -euo pipefail + +BASE_URL="${BASE_URL:-http://localhost:8080}" +PASS=0 +FAIL=0 + +check() { + local desc="$1" url="$2" method="${3:-GET}" expected="${4:-200}" + local status + if [ "$method" = "GET" ]; then + status=$(curl -s -o /dev/null -w '%{http_code}' "$url") + else + status=$(curl -s -o /dev/null -w '%{http_code}' -X "$method" "$url") + fi + if [ "$status" = "$expected" ]; then + echo "PASS: $desc (HTTP $status)" + PASS=$((PASS + 1)) + else + echo "FAIL: $desc (expected $expected, got $status)" + FAIL=$((FAIL + 1)) + fi +} + +check_json() { + local desc="$1" url="$2" method="${3:-GET}" field="$4" expected="$5" + local body + if [ "$method" = "GET" ]; then + body=$(curl -s "$url") + else + body=$(curl -s -X "$method" "$url") + fi + local value + value=$(echo "$body" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$field',''))" 2>/dev/null || echo "PARSE_ERROR") + if [ "$value" = "$expected" ]; then + echo "PASS: $desc ($field=$value)" + PASS=$((PASS + 1)) + else + echo "FAIL: $desc (expected $field=$expected, got $value)" + echo " Body: $(echo "$body" | head -c 200)" + FAIL=$((FAIL + 1)) + fi +} + +echo "=== Scenario 51: BMW IaC (DigitalOcean Mock) ===" +echo "" + +# Health check +check "healthz" "$BASE_URL/healthz" + +# Phase 1: Plan all resources +echo "" +echo "--- Phase 1: Plan ---" +check "plan database" "$BASE_URL/api/v1/iac/plan/database" "POST" +check "plan networking" "$BASE_URL/api/v1/iac/plan/networking" "POST" +check "plan app" "$BASE_URL/api/v1/iac/plan/app" "POST" +check "plan dns" "$BASE_URL/api/v1/iac/plan/dns" "POST" + +# Phase 2: Apply all resources +echo "" +echo "--- Phase 2: Apply ---" +check "apply database" "$BASE_URL/api/v1/iac/apply/database" "POST" +check "apply networking" "$BASE_URL/api/v1/iac/apply/networking" "POST" +check "apply app" "$BASE_URL/api/v1/iac/apply/app" "POST" +check "apply dns" "$BASE_URL/api/v1/iac/apply/dns" "POST" + +# Phase 3: Status +echo "" +echo "--- Phase 3: Status ---" +check "status all" "$BASE_URL/api/v1/iac/status" + +# Phase 4: Drift detection (version changed from 16 → 17) +echo "" +echo "--- Phase 4: Drift Detection ---" +check "drift database" "$BASE_URL/api/v1/iac/drift/database" "POST" + +# Phase 5: Idempotency — applying again after already applied +echo "" +echo "--- Phase 5: Idempotency ---" +check "apply database again (idempotent)" "$BASE_URL/api/v1/iac/apply/idempotent" "POST" + +# Phase 6: Destroy +echo "" +echo "--- Phase 6: Destroy ---" +check "destroy all" "$BASE_URL/api/v1/iac" "DELETE" + +echo "" +echo "=== Results: $PASS passed, $FAIL failed ===" +exit $FAIL