CluPilotCloud/docs/superpowers/plans/2026-07-25-customer-pipelin...

110 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# Customer Provisioning Pipeline (Subsystem B) Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:test-driven-development per task. Contract of record: `docs/specs/2026-07-25-engine-customer-pipeline.md` (the 15-step Section 3) + `docs/handoffs/2026-07-25-clupilot-engine-v1.0-handoff.md` (roadmap). This plan maps them to ordered TDD tasks + exact signatures.
**Goal:** A paid Stripe order provisions a running, secured, monitored Nextcloud instance automatically, on the shared orchestrator core built for Subsystem A.
**Architecture:** `pipeline='customer'` (subject = Order) on the existing DB-state-machine + tick orchestrator. 15 idempotent `CustomerStep` classes, VM lifecycle via the extended `ProxmoxClient` (API + guest-agent), DNS via Hetzner, TLS via Traefik/HTTP-01. Fully mocked in tests via the Fakes.
**Tech Stack:** Laravel 13.8, Livewire 3, Reverb, Redis provisioning queue, Pest 4 (sqlite `:memory:`), Stripe (test), phpseclib (present).
## Global Constraints
- R1/R2 class-based full-page Livewire · R3 token utilities only · R5 destructive→modal · R7 responsive · R8 container · R9 no emoji · R11 UUID URLs · R12 browser 0 console errors · R13 English routes · R14 self-hosted fonts · R15 Codex clean · R16 DE+EN identical keys · R17 blade `@php` rules.
- Tests: sqlite `:memory:` (phpunit force). Secrets never in git/events (`external_ref` never a secret); generated admin/DB passwords `forgetContext` + never plaintext-persisted.
- **Reuse the core, do NOT rebuild** (spec §0): StepResult (advance/retry/**poll**/fail), ProvisioningStep, RunRunner, ProvisioningRun/RunResource/StepEvent, PipelineRegistry, RemoteShell/WireguardHub, read-only ProxmoxClient.
- Test cmd: `docker compose run --rm --no-deps -u 1000:1000 -e HOME=/tmp app ./vendor/bin/pest`
## File Structure
**Models/Migrations:** `Customer, Order, Instance, DnsRecord, Backup, MonitoringTarget, OnboardingTask` (+ 7 migrations). Order implements `App\Provisioning\Contracts\ProvisioningSubject`.
**Steps:** `app/Provisioning/Steps/Customer/CustomerStep.php` (base) + 15 step classes (spec §3 table).
**Shared:** pull `recordResource()/hasResource()` into a `App\Provisioning\Steps\ManagesRunResources` trait used by both `HostStep` and `CustomerStep`.
**Services:** extend `App\Services\Proxmox\{ProxmoxClient,FakeProxmoxClient,HttpProxmoxClient}`; new `App\Services\Dns\{HetznerDnsClient(interface),HttpHetznerDnsClient,FakeHetznerDnsClient}`; new `App\Services\Traefik\{TraefikWriter(interface),SshTraefikWriter,FakeTraefikWriter}`.
**Intake:** `app/Http/Controllers/StripeWebhookController.php` + route (POST, no CSRF), `app/Actions/StartCustomerProvisioning.php`.
**UI:** rebind `app/Livewire/Admin/Provisioning.php`; customer dashboard stepper (`app/Livewire/Dashboard.php`); extend `StepAdvanced::broadcastOn()` + `routes/channels.php` (`customer.{id}.run`). `lang/{de,en}/provisioning.php`.
**Config:** `config/provisioning.php``plans`, `pipelines.customer`, `dns`, `traefik`.
## Interfaces (verbatim — consumed across tasks)
```php
// ProxmoxClient additions (spec §4)
public function nextVmid(): int;
public function cloneVm(string $node, int $templateVmid, int $newVmid, string $name): string; // UPID
public function setCloudInit(string $node, int $vmid, array $params): void;
public function resizeDisk(string $node, int $vmid, string $disk, string $size): void;
public function startVm(string $node, int $vmid): string; // UPID
public function vmStatus(string $node, int $vmid): array; // ['status'=>'running'|'stopped']
public function guestAgentPing(string $node, int $vmid): bool;
public function guestExec(string $node, int $vmid, string $command): array; // ['exitcode'=>int,'out-data'=>string]
public function taskStatus(string $node, string $upid): array; // ['status'=>...,'exitstatus'=>'OK'|...]
public function applyFirewall(string $node, int $vmid, array $rules): void;
// App\Services\Dns\HetznerDnsClient
public function upsertRecord(string $fqdn, string $type, string $value): string; // record_id
public function deleteRecord(string $recordId): void;
// App\Services\Traefik\TraefikWriter
public function write(string $subdomain, string $targetHost): void;
public function remove(string $subdomain): void;
public function certReachable(string $fqdn): bool;
```
**CustomerStep base helpers:** `order(run):Order`, `instance(run):Instance` (via `context('instance_id')`), `plan(run):array` (`config('provisioning.plans.'.$order->plan)`), `guest(ProxmoxClient,Instance,string $cmd):string` (guestExec, throws on exit≠0). RunResource kinds: `instance_id, vmid, dns_record_id, nc_admin, backup_job_id, monitoring_target_id`. Context keys: `instance_id, host_id, node, vmid, subdomain, plan`.
## Tasks (ordered — spec §9 build order)
### Task 1: Models + migrations (Phase 1)
Create 7 migrations + 7 models. Order `implements ProvisioningSubject` (`onProvisioningFailed`→status `failed`). Casts (uuid where URL-addressable; encrypted `nc_admin_ref` only stores username/ref never password; booleans). `subdomain` unique, `orders.stripe_event_id` unique. Capacity: add `Host::committedGb()` / free calc filtered on datacenter+active.
- [ ] Test: create order→run subject resolves; unique stripe_event_id enforced; instance subdomain unique; freeGb minus committed quotas. → implement → green → commit.
### Task 2: ManagesRunResources trait + CustomerStep base + config (Phase 2)
Extract `recordResource/hasResource` into trait; `HostStep` + `CustomerStep` use it. `CustomerStep` helpers above. `config/provisioning.php`: `plans` (start/team/business/enterprise with quota/disk/ram/cores/template_vmid), `pipelines.customer` (15 classes — textual, built incrementally), `dns`, `traefik`.
- [ ] Test: trait keeps HostStep tests green; CustomerStep resolves order/instance/plan. → implement → green → commit.
### Task 3: ProxmoxClient extension (Phase 3)
Add the §4 methods to interface + `FakeProxmoxClient` (deterministic: nextVmid counter, cloneVm→UPID string, taskStatus→stopped/OK, vmStatus→running after start, guestAgentPing→true, guestExec→exit 0 configurable-fail, applyFirewall records) + `HttpProxmoxClient` (real endpoints, UPID poll via `/tasks/{upid}/status`).
- [ ] Test (Fake): clone→vmid+upid; taskStatus OK; guestExec exit code; configurable failure. → implement → green → commit.
### Task 4: Steps 12 (Phase 4)
`ValidateOrder` (paid? plan known? datacenter valid? → advance/[F]), `ReserveResources` (idempotent on existing instance; placement = first active host in datacenter with free committed ≥ quota; create instance; context instance_id/host_id/node; [E] instance_id; [F] no_capacity).
- [ ] Tests: validate fail on unpaid/unknown plan; reserve creates instance once (idempotent), picks datacenter host, no_capacity fails. → green → commit.
### Task 5: Steps 36 (Phase 5) — against FakeProxmox
`CloneVirtualMachine` (idempotent on vmid; **[E] vmid before poll**; poll taskStatus; [F] template_missing/clone_failed), `ConfigureCloudInit` (setCloudInit+resizeDisk idempotent), `StartVirtualMachine` (idempotent on running; poll), `WaitForGuestAgent` (poll ping; own deadline→[F]).
- [ ] Tests each: [E] persistence before poll, idempotency (crash after recordResource→no dup vmid), poll vs advance, fail classification. → green → commit.
### Task 6: Steps 710 (Phase 6) — via guestExec fake
`ConfigureNetwork` (guest verify + applyFirewall), `DeployApplicationStack` (write compose .env with generated DB pw → forgetContext; compose up; poll healthy), `ConfigureNextcloud` (occ trusted_domains/cron/install-guard idempotent), `CreateCustomerAdmin` (idempotent on nc_admin; generate pw via OC_PASS env; [E] nc_admin=username; **never persist password**).
- [ ] Tests: secrets never plaintext-persisted (context scrubbed, events carry no secret); idempotency; admin created once. → green → commit.
### Task 7: Step 11 + DNS/Traefik services (Phase 7)
`HetznerDnsClient` (interface+Fake+Http), `TraefikWriter` (interface+Fake+SSH). `ConfigureDnsAndTls`: upsert DNS **[E] dns_record_id** + dns_records row; Traefik write + route_written; **[P] certReachable** → cert_ok; [F] cert_timeout / invalid domain; DNS 5xx→retry.
- [ ] Tests: dns record recorded before advance; cert poll→advance; timeout→fail. → green → commit.
### Task 8: Steps 1215 + e2e (Phase 8)
`RegisterBackup` ([E] backup_job_id), `RegisterMonitoring` ([E] monitoring_target_id), `RunAcceptanceChecks` (gate: cert/status.php/admin-login/backup/monitoring; [F] acceptance_failed:<what>), `CompleteProvisioning` (instance+order active, onboarding_tasks, notify; last step→completed). **Mocked e2e:** paid order → drive runner → completed, instance active, resources each once, no secret leaks, customer access only after 14.
- [ ] Tests + e2e + crash-idempotency. → green → commit.
### Task 9: Stripe webhook (Phase 9)
`StripeWebhookController` (verify signature in non-test; test-mode path), idempotent on `orders.stripe_event_id` unique → create customer+order+run+dispatch. `StartCustomerProvisioning` action (transaction, dispatch after commit).
- [ ] Tests: duplicate event → one run; paid event → run created + AdvanceRunJob pushed. → green → commit.
### Task 10: Live progress (Phase 10)
Rebind `Admin\Provisioning` to real runs+events (live via `admin.runs`). Customer dashboard: bind stepper to logged-in customer's run; add `PrivateChannel('customer.{id}.run')` to `StepAdvanced::broadcastOn()` (+ channels.php authz: own customer only); onboarding_tasks feed. Access to cloud only when `active`.
- [ ] Tests: admin provisioning renders real runs; customer sees only own run; channel authz. → green → commit.
### Task 11: Verify (Phase 11)
DE+EN `provisioning.step.*` (+ any UI keys) identical keys; full Pest green; R12 Puppeteer (admin provisioning + customer dashboard) 0 console errors; R15 Codex `--scope branch --base <pre-B commit>` loop until clean; seeder demo order/run gated local/testing; commit + push (token inline).
## Self-Review
- **Spec coverage:** §1 models→T1; §2 base→T2; §4 client→T3; §3 steps 115→T4T8; §5 services→T7; §6 Stripe→T9; §7 live→T10; §8 config→T2; §9 order→T1T11; §10 DoD→T8/T11; §11 watch-items (poll, recordResource-before-external, guest-agent, traefik path, secrets)→embedded in T5T8.
- **Placeholder scan:** none — each task has concrete conditions + tests referencing the spec table.
- **Type consistency:** ProxmoxClient/HetznerDnsClient/TraefikWriter signatures fixed above and reused; RunResource kinds + context keys enumerated once.