package hostops import ( "fmt" "sort" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/registry" ) // Claim activation and seeded instances (seeded-instances spec §3.4, §5.6; // merge review S2). // // A claim console refuses every import door, runs no seed sweep and // finishes no seed, and a claim-armed box arms neither the swap nor the // rollback. So a seed that is unfinished when claims come on is stranded: // an import stays gated with nothing able to commit it, put its old route // back or clean it up. Activation therefore refuses while any unfinished // seed state exists, and the claim's own seed algorithm (#250) can lift // this once it is built. // // Publish makes the check, under the collecting manifest, which is the // record-writer freeze. Three things count, and each is read from evidence // activation already authenticates: // // - an unfinished seed on a record, from the census's own record read // (provision.SeedSource.Unfinished): every seed but a placed --from // copy and a committed import; // - an import journal row on a box; // - a gated route on a box. // // The last two are box-local: a committed import's journal row and gate // stay until the box's next pass clears them, and a rollback's journal row // outlives its record. So each box says them in its signed census report // (CensusReport.Seeds). A report that does not carry them is not read as // "none": activation refuses, as it does for a report that is incomplete. // CensusSeeds is a box's own seed state at its census: the hosts its import // journal holds a row for, in any state, and the hosts whose route carries // an import gate, open or closed. Both sorted. Empty lists are a box that // has none; a nil *CensusSeeds on the report is a box that did not say. type CensusSeeds struct { Journaled []string `json:"journaled"` Gated []string `json:"gated"` } // SeedStateReader is what a box's census collector reads its seed state // from: the box's registry. Both reads are served by a registry opened // through root's registry child as well as one opened directly. type SeedStateReader interface { List() ([]registry.Route, error) ImportJournalHosts() ([]string, error) } // CollectCensusSeeds reads a box's seed state for its census report. An // error is a box that cannot say: the collector then leaves // CensusReport.Seeds nil, and activation refuses. func CollectCensusSeeds(r SeedStateReader) (*CensusSeeds, error) { journaled, err := r.ImportJournalHosts() if err != nil { return nil, err } routes, err := r.List() if err != nil { return nil, err } out := &CensusSeeds{Journaled: append([]string{}, journaled...), Gated: []string{}} for _, rt := range routes { if rt.ImportGate != "" { out.Gated = append(out.Gated, rt.Host) } } sort.Strings(out.Journaled) sort.Strings(out.Gated) return out, nil } // SeedActivationError is an activation refused over seed state. It is // ErrPending when a seed is unfinished (finish or abandon it, then activate // again) and ErrIdentity when a box's report carries no seed state (the // evidence is incomplete, as for any other incomplete report). // // DRAFT COPY: the four sentences below are operator-facing and have not // been through copy review. They are listed, with this site, in the merge // report's review-fixes section. type SeedActivationError struct { base error text string } func (e *SeedActivationError) Error() string { return e.base.Error() + ": " + e.text } func (e *SeedActivationError) Unwrap() error { return e.base } func seedEvidenceMissing(box string) error { return &SeedActivationError{ErrIdentity, fmt.Sprintf("box %s reported no seed state, so an import in flight there cannot be ruled out", box)} } func seedJournaled(box, host string) error { return &SeedActivationError{ErrPending, fmt.Sprintf("box %s still holds an import journal row for %s", box, host)} } func seedGated(box, host string) error { return &SeedActivationError{ErrPending, fmt.Sprintf("box %s still gates %s for an import", box, host)} } func seedUnfinished(host string, seed *provision.SeedSource) error { return &SeedActivationError{ErrPending, fmt.Sprintf("%s has an unfinished seed (%s, %s)", host, seed.Kind, seed.State)} } // activationSeedRefusal is Publish's seed check over one census: nil when // every box reported its seed state and no box and no record holds any // that is unfinished. Boxes and hosts are judged in name order, so the // answer for one census is always the same one. func activationSeedRefusal(c Census) error { boxes := make([]string, 0, len(c.Reports)) for box := range c.Reports { boxes = append(boxes, box) } sort.Strings(boxes) // Evidence first: with any box unknown, nothing else can clear it. for _, box := range boxes { if c.Reports[box].Seeds == nil { return seedEvidenceMissing(box) } } for _, box := range boxes { seeds := c.Reports[box].Seeds for _, host := range sortedHosts(seeds.Journaled) { return seedJournaled(box, host) } for _, host := range sortedHosts(seeds.Gated) { return seedGated(box, host) } } records := append([]Record(nil), c.Records...) sort.Slice(records, func(i, j int) bool { return records[i].Holder.Host < records[j].Holder.Host }) for _, r := range records { if r.Holder.Family != "instance" { continue } var rec provision.Record if err := decode(r.Body, &rec); err != nil { return ErrIdentity } if rec.Seed.Unfinished() { return seedUnfinished(r.Holder.Host, rec.Seed) } } return nil } func sortedHosts(hosts []string) []string { out := append([]string(nil), hosts...) sort.Strings(out) return out }