package main import ( "context" "encoding/json" "sort" "strings" "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" ) // sendable is a declaration as a machine is sent it: its resources and, for an adopted node, its // mode and which modules were taken on it (novox/hq ADR 0100). // // **Body is the only place the envelope is marshalled.** It was written by hand at every send site, // in the digest the mesh compares, and in `plan --json`; a key added at one and not another would // make a machine look out of date for ever, or send something `plan` never showed. type sendable struct { Resources []map[string]any // Adoption is nil for a converged node, and then the body is byte for byte what it was before // adoption existed: an older host parses the envelope strictly and would refuse the key. Adoption *adoptionEnvelope } // adoptionEnvelope is what an adopted node is told about its mode. Taken is every module taken on // it that it runs; Untaken is, for every module it runs that is not taken, the ids of every one of // that module's resources — what the host keeps as found until the module is taken (ADR 0103). Ids // rather than a rule to split them by, because a module's name may contain a dot. type adoptionEnvelope struct { Taken []string `json:"taken"` Untaken map[string][]string `json:"untaken,omitempty"` } // Body is the declaration's bytes, as sent and as digested. func (s sendable) Body() ([]byte, error) { envelope := map[string]any{"declaration": 1, "resources": s.Resources} if s.Adoption != nil { envelope["adoption"] = s.Adoption } // An empty declaration is deliberate here — the node owns nothing the mesh put there // (novox/hq issue 127) — and the host refuses an empty body unless it is told the emptiness // is meant, so a truncated or mis-composed body is never mistaken for "own nothing". if len(s.Resources) == 0 { envelope["owns_nothing"] = true } return json.Marshal(envelope) } // adoptionOf is the envelope for a node, nil when it is converged. func adoptionOf(ctx context.Context, inv *inventory.Inventory, record inventory.Node, plan catalogue.Resolution, composed catalogue.Composed) (*adoptionEnvelope, error) { if !record.Adopted { return nil, nil } taken, err := inv.Taken(ctx, record.Name) if err != nil { return nil, err } return adoptionFor(plan, taken, composed), nil } // adoptionFor is the envelope computed from what was taken and who owns each resource. // // Every module the node runs that is not taken is untaken — including one pulled in by another // rather than assigned: what is found is kept until its module is taken, whoever put it there. func adoptionFor(plan catalogue.Resolution, taken []string, composed catalogue.Composed) *adoptionEnvelope { isTaken := map[string]bool{} for _, m := range taken { isTaken[m] = true } out := &adoptionEnvelope{Taken: []string{}} runs := map[string]bool{} for _, m := range plan.Modules { runs[m.Module] = true if isTaken[m.Module] { out.Taken = append(out.Taken, m.Module) } } sort.Strings(out.Taken) for _, r := range composed.Resources { id, _ := r["id"].(string) module, owned := composed.Owner[id] // Every kind, not only files and containers (novox/hq ADR 0103): a directory, a service, a // container mounting what was found and an action run in a held container all reach what // the machine already has. What the mesh declares of its own is never held. if !owned || !runs[module] || isTaken[module] || strings.HasPrefix(id, catalogue.AdoptionPrefix) { continue } if out.Untaken == nil { out.Untaken = map[string][]string{} } out.Untaken[module] = append(out.Untaken[module], id) } return out }