package apply import "fmt" // Result is what one apply did: which resources it brought to the declared state, and which it // removed because they were applied before and are no longer declared. type Result struct { Applied []Record Removed []Record } // Apply makes this machine match decl, records what it did, and removes what it once applied and // decl no longer names. // // The three properties that govern it are each a recorded decision, not a preference: // // - A failed step fails the apply (ADR 0008). This function stops at the first resource it // cannot bring to state and returns the error. It does not log and continue: a partial apply // that reports success is the mesh's most expensive shape. // - What was applied is recorded after it works, never before (ADR 0035). Each success is // appended to what the store will hold; a failure leaves the machine in whatever state it // reached, and the store is saved reflecting exactly that — never more. // - The host is authoritative over its own footprint and inert everywhere else (ADR 0043). // Removal touches only resources the store recorded as created by the host. // // identity is this node's own name. A declaration addressed to another node is refused; a host // with no identity yet — the first node — applies whatever it is handed, because it has nothing // to check against (ADR 0043). func Apply(decl Declaration, identity string, store *Store, appliers map[string]Applier) (Result, error) { if decl.For != "" && identity != "" && decl.For != identity { return Result{}, fmt.Errorf( "declaration is for %q and this node is %q — refused, a node applies only what is "+ "addressed to it", decl.For, identity) } prior := store.Records() declared := make(map[string]bool, len(decl.Resources)) for _, r := range decl.Resources { declared[r.ID] = true } // Who created what, carried across applies. "created" must be sticky: once the host brought a // resource into being, it stays the creator through every re-apply, or a second apply would // see the resource already present, record created=false, and then decline to remove // something it in fact created. That flip would leak a host-created resource on the next // declaration that drops it — reported handled, actually orphaned. priorCreated := make(map[string]bool, len(prior)) for _, rec := range prior { priorCreated[rec.ID] = rec.Created } // Apply in the stated order, recording each success as it lands. applied := make([]Record, 0, len(decl.Resources)) for _, r := range decl.Resources { a, ok := appliers[r.Type] if !ok { // Parse already refused unknown types, so this is a host wired inconsistently with // its own shape table — a bug, surfaced rather than skipped. err := fmt.Errorf("resource %q is a %q with no applier — refusing", r.ID, r.Type) saveMerged(store, prior, applied) return Result{}, err } created, err := a.Apply(r) if err != nil { // The resource is not at the declared state. Record what did land (this one did not, // so it is not appended), persist that, and fail. saveMerged(store, prior, applied) return Result{}, fmt.Errorf("applying %s %q: %w", r.Type, r.ID, err) } applied = append(applied, Record{ ID: r.ID, Type: r.Type, Ref: r.ref(), Created: created || priorCreated[r.ID], }) } // Remove what was applied before and is no longer declared, in reverse application order so // a file goes before the directory that held it. Only host-created resources are touched. removedIDs := map[string]bool{} var removed []Record for i := len(prior) - 1; i >= 0; i-- { rec := prior[i] if declared[rec.ID] { continue } if rec.Created { if a, ok := appliers[rec.Type]; ok { if err := a.Remove(rec); err != nil { // A removal that failed leaves the resource present. That is a failed step, // so the apply fails — and the store must reflect reality: the declared set // that landed, plus every undeclared prior record not yet removed (this one // included), in application order. store.replace(survivorsAfterFailedRemoval(applied, prior, declared, removedIDs)) _ = store.Save() return Result{}, fmt.Errorf("removing %s %q: %w", rec.Type, rec.ID, err) } } } removedIDs[rec.ID] = true removed = append(removed, rec) } // Success: the store now holds exactly the declared set, freshly recorded. store.replace(applied) if err := store.Save(); err != nil { return Result{}, fmt.Errorf("apply succeeded but its record could not be saved: %w", err) } return Result{Applied: applied, Removed: removed}, nil } // survivorsAfterFailedRemoval is what the machine still holds when a removal fails: the declared // set that was just applied, plus every prior undeclared record not successfully removed — // including the one whose removal failed — kept in application order. func survivorsAfterFailedRemoval(applied, prior []Record, declared, removedIDs map[string]bool) []Record { survivors := append([]Record{}, applied...) for _, rec := range prior { if declared[rec.ID] || removedIDs[rec.ID] { continue } survivors = append(survivors, rec) } return survivors } // saveMerged persists prior records overlaid with what was just applied, for the failure path: // the machine holds both the untouched prior resources and the ones that landed before the // failure, so the store must record both. func saveMerged(store *Store, prior, applied []Record) { store.replace(mergeByID(prior, applied)) _ = store.Save() } // mergeByID returns base with overlay applied on top, overlay winning on a shared id, preserving // base order and appending overlay-only records. func mergeByID(base, overlay []Record) []Record { byID := make(map[string]Record, len(overlay)) for _, r := range overlay { byID[r.ID] = r } out := make([]Record, 0, len(base)+len(overlay)) seen := map[string]bool{} for _, r := range base { if o, ok := byID[r.ID]; ok { out = append(out, o) seen[r.ID] = true continue } out = append(out, r) } for _, r := range overlay { if !seen[r.ID] { out = append(out, r) } } return out }