package apply import ( "encoding/json" "fmt" "os" "path/filepath" ) // Store is the host's record of what it has applied to this machine, and it is authoritative // while disconnected (novox/hq 03-DESIGN/01-to-be/05-the-node-host.md). It is not a cache of // the control plane: it is what makes removal possible — the host removes what it previously // applied and is no longer declared, and it knows what it applied because this recorded it // (ADR 0043). // // The records are kept in application order, so removal can run in reverse — a file goes before // the directory that holds it. type Store struct { path string records []Record } // Record is one applied resource, holding just enough to remove it. // // Created is the whole of the data-loss guard. The host is authoritative over its own footprint // and inert everywhere else (ADR 0043), so it removes only what it created. A directory it // merely adopted — one that already held workload data — is recorded Created:false and is never // removed, which is the same rule that ADR 0018 exists to enforce: never act on a path you did // not create. type Record struct { ID string `json:"id"` Type string `json:"type"` // Ref is how the resource is found again for removal: a filesystem path for files and // directories, a container or network name for those. Ref string `json:"ref"` Created bool `json:"created"` } // LoadStore reads the store at path. A missing file is an empty store, not an error: a machine // the host has never applied to has applied nothing, which is a fact with a true empty answer. func LoadStore(path string) (*Store, error) { s := &Store{path: path} raw, err := os.ReadFile(path) if os.IsNotExist(err) { return s, nil } if err != nil { return nil, fmt.Errorf("reading the applied-state store %s: %w", path, err) } var records []Record if err := json.Unmarshal(raw, &records); err != nil { return nil, fmt.Errorf( "the applied-state store %s is not readable — refusing rather than treating a machine "+ "as blank when it is not: %w", path, err) } s.records = records return s, nil } // Records returns the applied resources in application order. func (s *Store) Records() []Record { return s.records } // Save writes the store atomically: a torn store is a machine that has forgotten what it holds, // so the write goes to a sibling temp file and is renamed into place. func (s *Store) Save() error { if s.path == "" { return nil } if err := os.MkdirAll(filepath.Dir(s.path), 0o700); err != nil { return fmt.Errorf("preparing the store directory: %w", err) } raw, err := json.MarshalIndent(s.records, "", " ") if err != nil { return fmt.Errorf("encoding the store: %w", err) } tmp := s.path + ".tmp" if err := os.WriteFile(tmp, append(raw, '\n'), 0o600); err != nil { return fmt.Errorf("writing the store: %w", err) } if err := os.Rename(tmp, s.path); err != nil { return fmt.Errorf("committing the store: %w", err) } return nil } // replace sets the records to exactly what was just applied, in order. func (s *Store) replace(records []Record) { s.records = records }