| 1 | package hostops |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "sort" |
| 6 | |
| 7 | "github.com/carlosframework/platform/internal/provision" |
| 8 | "github.com/carlosframework/platform/internal/registry" |
| 9 | ) |
| 10 | |
| 11 | // Claim activation and seeded instances (seeded-instances spec §3.4, §5.6; |
| 12 | // merge review S2). |
| 13 | // |
| 14 | // A claim console refuses every import door, runs no seed sweep and |
| 15 | // finishes no seed, and a claim-armed box arms neither the swap nor the |
| 16 | // rollback. So a seed that is unfinished when claims come on is stranded: |
| 17 | // an import stays gated with nothing able to commit it, put its old route |
| 18 | // back or clean it up. Activation therefore refuses while any unfinished |
| 19 | // seed state exists, and the claim's own seed algorithm (#250) can lift |
| 20 | // this once it is built. |
| 21 | // |
| 22 | // Publish makes the check, under the collecting manifest, which is the |
| 23 | // record-writer freeze. Three things count, and each is read from evidence |
| 24 | // activation already authenticates: |
| 25 | // |
| 26 | // - an unfinished seed on a record, from the census's own record read |
| 27 | // (provision.SeedSource.Unfinished): every seed but a placed --from |
| 28 | // copy and a committed import; |
| 29 | // - an import journal row on a box; |
| 30 | // - a gated route on a box. |
| 31 | // |
| 32 | // The last two are box-local: a committed import's journal row and gate |
| 33 | // stay until the box's next pass clears them, and a rollback's journal row |
| 34 | // outlives its record. So each box says them in its signed census report |
| 35 | // (CensusReport.Seeds). A report that does not carry them is not read as |
| 36 | // "none": activation refuses, as it does for a report that is incomplete. |
| 37 | |
| 38 | // CensusSeeds is a box's own seed state at its census: the hosts its import |
| 39 | // journal holds a row for, in any state, and the hosts whose route carries |
| 40 | // an import gate, open or closed. Both sorted. Empty lists are a box that |
| 41 | // has none; a nil *CensusSeeds on the report is a box that did not say. |
| 42 | type CensusSeeds struct { |
| 43 | Journaled []string `json:"journaled"` |
| 44 | Gated []string `json:"gated"` |
| 45 | } |
| 46 | |
| 47 | // SeedStateReader is what a box's census collector reads its seed state |
| 48 | // from: the box's registry. Both reads are served by a registry opened |
| 49 | // through root's registry child as well as one opened directly. |
| 50 | type SeedStateReader interface { |
| 51 | List() ([]registry.Route, error) |
| 52 | ImportJournalHosts() ([]string, error) |
| 53 | } |
| 54 | |
| 55 | // CollectCensusSeeds reads a box's seed state for its census report. An |
| 56 | // error is a box that cannot say: the collector then leaves |
| 57 | // CensusReport.Seeds nil, and activation refuses. |
| 58 | func CollectCensusSeeds(r SeedStateReader) (*CensusSeeds, error) { |
| 59 | journaled, err := r.ImportJournalHosts() |
| 60 | if err != nil { |
| 61 | return nil, err |
| 62 | } |
| 63 | routes, err := r.List() |
| 64 | if err != nil { |
| 65 | return nil, err |
| 66 | } |
| 67 | out := &CensusSeeds{Journaled: append([]string{}, journaled...), Gated: []string{}} |
| 68 | for _, rt := range routes { |
| 69 | if rt.ImportGate != "" { |
| 70 | out.Gated = append(out.Gated, rt.Host) |
| 71 | } |
| 72 | } |
| 73 | sort.Strings(out.Journaled) |
| 74 | sort.Strings(out.Gated) |
| 75 | return out, nil |
| 76 | } |
| 77 | |
| 78 | // SeedActivationError is an activation refused over seed state. It is |
| 79 | // ErrPending when a seed is unfinished (finish or abandon it, then activate |
| 80 | // again) and ErrIdentity when a box's report carries no seed state (the |
| 81 | // evidence is incomplete, as for any other incomplete report). |
| 82 | // |
| 83 | // DRAFT COPY: the four sentences below are operator-facing and have not |
| 84 | // been through copy review. They are listed, with this site, in the merge |
| 85 | // report's review-fixes section. |
| 86 | type SeedActivationError struct { |
| 87 | base error |
| 88 | text string |
| 89 | } |
| 90 | |
| 91 | func (e *SeedActivationError) Error() string { return e.base.Error() + ": " + e.text } |
| 92 | func (e *SeedActivationError) Unwrap() error { return e.base } |
| 93 | |
| 94 | func seedEvidenceMissing(box string) error { |
| 95 | return &SeedActivationError{ErrIdentity, fmt.Sprintf("box %s reported no seed state, so an import in flight there cannot be ruled out", box)} |
| 96 | } |
| 97 | |
| 98 | func seedJournaled(box, host string) error { |
| 99 | return &SeedActivationError{ErrPending, fmt.Sprintf("box %s still holds an import journal row for %s", box, host)} |
| 100 | } |
| 101 | |
| 102 | func seedGated(box, host string) error { |
| 103 | return &SeedActivationError{ErrPending, fmt.Sprintf("box %s still gates %s for an import", box, host)} |
| 104 | } |
| 105 | |
| 106 | func seedUnfinished(host string, seed *provision.SeedSource) error { |
| 107 | return &SeedActivationError{ErrPending, fmt.Sprintf("%s has an unfinished seed (%s, %s)", host, seed.Kind, seed.State)} |
| 108 | } |
| 109 | |
| 110 | // activationSeedRefusal is Publish's seed check over one census: nil when |
| 111 | // every box reported its seed state and no box and no record holds any |
| 112 | // that is unfinished. Boxes and hosts are judged in name order, so the |
| 113 | // answer for one census is always the same one. |
| 114 | func activationSeedRefusal(c Census) error { |
| 115 | boxes := make([]string, 0, len(c.Reports)) |
| 116 | for box := range c.Reports { |
| 117 | boxes = append(boxes, box) |
| 118 | } |
| 119 | sort.Strings(boxes) |
| 120 | // Evidence first: with any box unknown, nothing else can clear it. |
| 121 | for _, box := range boxes { |
| 122 | if c.Reports[box].Seeds == nil { |
| 123 | return seedEvidenceMissing(box) |
| 124 | } |
| 125 | } |
| 126 | for _, box := range boxes { |
| 127 | seeds := c.Reports[box].Seeds |
| 128 | for _, host := range sortedHosts(seeds.Journaled) { |
| 129 | return seedJournaled(box, host) |
| 130 | } |
| 131 | for _, host := range sortedHosts(seeds.Gated) { |
| 132 | return seedGated(box, host) |
| 133 | } |
| 134 | } |
| 135 | records := append([]Record(nil), c.Records...) |
| 136 | sort.Slice(records, func(i, j int) bool { return records[i].Holder.Host < records[j].Holder.Host }) |
| 137 | for _, r := range records { |
| 138 | if r.Holder.Family != "instance" { |
| 139 | continue |
| 140 | } |
| 141 | var rec provision.Record |
| 142 | if err := decode(r.Body, &rec); err != nil { |
| 143 | return ErrIdentity |
| 144 | } |
| 145 | if rec.Seed.Unfinished() { |
| 146 | return seedUnfinished(r.Holder.Host, rec.Seed) |
| 147 | } |
| 148 | } |
| 149 | return nil |
| 150 | } |
| 151 | |
| 152 | func sortedHosts(hosts []string) []string { |
| 153 | out := append([]string(nil), hosts...) |
| 154 | sort.Strings(out) |
| 155 | return out |
| 156 | } |
| 157 | |