Planner
The Planner takes the diff output and the dependency graph and produces an ordered execution plan. It groups changes by graph level for parallel execution.
generatePlan()
Section titled “generatePlan()”class Planner { generatePlan(config: Config, state: StateFile, graph: Graph, diffs: DiffResult): Plan}Inputs
Section titled “Inputs”| Input | Source | Description |
|---|---|---|
config |
User’s infrastructure.ts |
Desired resource definitions |
state |
.kyku/state.<env>.json |
Last known cloud state |
graph |
GraphBuilder.build() |
Dependency graph with levels |
diffs |
DiffEngine.diff() |
Changes per resource |
Output
Section titled “Output”interface Plan { changes: Change[]; // Ordered by graph level counters: PlanCounters; // Summary: creates, updates, replaces, destroys graph: Graph; // Dependency graph for reference environment: string; // Workspace name}
interface PlanCounters { create: number; update: number; replace: number; destroy: number; noop: number;}State vs Cloud Logic
Section titled “State vs Cloud Logic”The planner decides what’s new vs existing based on state:
- Has
providerIdin state → queries cloud viaprovider.readState(id, providerId)to detect drift - No
providerIdin state → always showscreate(never queries cloud)
This is critical for idempotency. Querying the cloud for resources not in state would cause spurious “no-op” results.
Ordering
Section titled “Ordering”Changes are ordered by graph level:
Level 0: [create Vpc, create SshKey] ← parallelLevel 1: [create Subnet, create SecurityGroup]Level 2: [create Vm, create Database]Level 3: [create LoadBalancer]Each level waits for all lower levels to complete. Resources within a level execute in parallel (up to --parallelism limit).
Destroy Order
Section titled “Destroy Order”Destroy follows reverse level order:
Level 3: [destroy LoadBalancer]Level 2: [destroy Vm, destroy Database]Level 1: [destroy SecurityGroup, destroy Subnet]Level 0: [destroy Vpc, destroy SshKey]Plan Save/Load
Section titled “Plan Save/Load”Plans can be serialized to JSON files and loaded later:
# Save plankyku plan --out plan.json
# Apply from saved plankyku apply --plan plan.jsonThe engine’s apply() accepts an optional plan?: Plan 4th parameter — if provided, it uses pre-computed changes and skips the plan() call.
Idempotent Create
Section titled “Idempotent Create”Every provider create() method checks if the resource already exists before creating:
async create(resource: Vpc): Promise<CloudState> { const existing = await client.get(`/resources?name=${resource.name}`); if (existing.resources?.length > 0) { return this.read(resource, String(existing.resources[0].id)); } // ... create new}Exception: Purely logical resources (e.g., TargetGroup that just defines a label) don’t need this check.
Provider ID Management
Section titled “Provider ID Management”Critical Rule
Section titled “Critical Rule”Never pass Kyku resource UUIDs to cloud APIs.
| ID Type | Format | Example |
|---|---|---|
| Kyku ID | UUID/string | "hetzner-test" or "p1s9hutn" |
| Cloud ID | Provider-native | 12224210 (Hetzner), vpc-0abc123 (AWS) |
The readState by name is the fallback for resolving cloud IDs from Kyku references.
Destroy Provider ID Resolution
Section titled “Destroy Provider ID Resolution”If stateResource.providerId is non-numeric (stale Kyku ID from early versions), look it up via provider.readState(stub) before passing to provider.destroy().
Plan Output Format
Section titled “Plan Output Format”Human-readable plan output:
Plan: 3 to create, 1 to update, 0 to replace, 1 to destroy, 2 no-op
Changes: + create Vpc "main-vpc" (level 0) + create SecurityGroup "web-sg" (level 1) + create Vm "web-server" (level 2) ~ update LoadBalancer "web-lb" (level 3) port: 80 → 443 - destroy Vm "old-worker" (level 2) ✓ no-op Database "app-db"JSON output with --json flag:
{ "changes": [ { "action": "create", "resourceId": "vpc-main", "level": 0 }, { "action": "create", "resourceId": "web-sg", "level": 1 } ], "counters": { "create": 3, "update": 1, "destroy": 1, "replace": 0, "noop": 2 }}