| 1 | package steering |
| 2 | |
| 3 | // deployment.go — this package's ONE exported converge entry point. |
| 4 | // |
| 5 | // Everything else here is unexported by design: desiredState is a pure fold |
| 6 | // (steering.go) and converge takes a set that only that fold can build |
| 7 | // (route53.go), so a caller outside this package cannot assemble the argument |
| 8 | // converge needs. ConvergeDeployment is the seam that closes that gap for the |
| 9 | // host housekeeper (internal/host/steering_loop.go) — it takes what a BOX has |
| 10 | // (registry rows and raw edges-file bodies), folds, and converges, in one |
| 11 | // call that cannot be told to converge a set the fold did not produce. |
| 12 | // |
| 13 | // Note on the client parameter's type. route53API stays unexported even |
| 14 | // though this function is exported: every method in it is an exported SDK |
| 15 | // method name, so any type implementing them satisfies it from any package — |
| 16 | // Go checks method sets, not the interface's own visibility. The host caller |
| 17 | // therefore needs nothing more than NewClient (route53.go), whose concrete |
| 18 | // *route53.Client it can pass straight in, and no test outside this package |
| 19 | // ever needs to fake AWS. Exporting the interface would add surface with no |
| 20 | // consumer; the constructor is the seam, and it already exists. |
| 21 | |
| 22 | import ( |
| 23 | "context" |
| 24 | "log" |
| 25 | |
| 26 | "github.com/carlosframework/platform/internal/registry" |
| 27 | ) |
| 28 | |
| 29 | // ConvergeDeployment folds this deployment's routes and edges files into the |
| 30 | // desired steering state and converges the zone to it. |
| 31 | // |
| 32 | // edgesFiles are the RAW bodies of control/edges/<box>.json, one per armed |
| 33 | // box, in the caller's listing order (the host loader sorts by key, so the |
| 34 | // order — and therefore the fold — is stable tick to tick). |
| 35 | // |
| 36 | // The two failure classes an edges file can have are handled in DIFFERENT |
| 37 | // places on purpose, and the split is the durable half of Task 5's shrink- |
| 38 | // guard finding: |
| 39 | // |
| 40 | // - a file that could not be READ (the listing failed, or a Get failed) is |
| 41 | // a VISIBILITY problem: the deployment cannot see all of its edges, so a |
| 42 | // fold built from what came back is SHORT, and a short fold deletes the |
| 43 | // records of every box that went missing. That case must never reach this |
| 44 | // function — the caller fails closed and returns the error instead (see |
| 45 | // internal/host/steering_loop.go's loadEdgesFileBodies). |
| 46 | // - a file that was read but does not PARSE is a CONTENT problem, and it is |
| 47 | // handled HERE by skipping it and saying so. A single box publishing a |
| 48 | // corrupt or future-shaped body must not wedge the whole deployment's |
| 49 | // steering, and the skip is bounded: the box simply contributes no |
| 50 | // records this pass, which is the same as being unarmed. |
| 51 | // |
| 52 | // The distinction is not cosmetic. Both end in "one fewer edge in the fold", |
| 53 | // but only the read failure means the deployment does not KNOW what it has — |
| 54 | // and converge's own guards (the empty-set refusal and the proportional |
| 55 | // shrink limit) are the last line, not the first. This is the first. |
| 56 | func ConvergeDeployment(ctx context.Context, client route53API, zoneID string, routes []registry.Route, edgesFiles [][]byte, appsDomain string) error { |
| 57 | edges := make([]edgesFileView, 0, len(edgesFiles)) |
| 58 | for i, body := range edgesFiles { |
| 59 | f, err := parseEdgesFileView(body) |
| 60 | if err != nil { |
| 61 | // The bodies arrive without their keys (the caller hands over |
| 62 | // []byte by design, so this package never touches a store), so |
| 63 | // the position is the handle an operator gets: bodies are passed |
| 64 | // in the caller's sorted listing order, making "file 2 of 3" the |
| 65 | // 2nd key under control/edges/. The length disambiguates an empty |
| 66 | // object from a truncated one. |
| 67 | log.Printf("steering: skipping edges file %d of %d (%d bytes): %v — that box contributes no records this pass", i+1, len(edgesFiles), len(body), err) |
| 68 | continue |
| 69 | } |
| 70 | edges = append(edges, f) |
| 71 | } |
| 72 | return converge(ctx, client, zoneID, desiredState(routes, edges, appsDomain)) |
| 73 | } |
| 74 | |