| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "crypto/ed25519" |
| 6 | "os" |
| 7 | "path/filepath" |
| 8 | "testing" |
| 9 | |
| 10 | "github.com/carlosframework/platform/internal/registry" |
| 11 | "github.com/carlosframework/platform/internal/release" |
| 12 | "github.com/carlosframework/platform/internal/rollout" |
| 13 | ) |
| 14 | |
| 15 | // Settings spec §4.3 through updatesPassOption: the option is built ONCE, |
| 16 | // as fastPassOpts builds it, and still reads swap-failures.json at the start |
| 17 | // of every pass. A build is failed for a host when the book blocks it or |
| 18 | // holds the host down on it; a served build is exempt from Blocks only. |
| 19 | func TestUpdatesPassOptionReadsTheBookEveryPass(t *testing.T) { |
| 20 | t.Parallel() |
| 21 | ctx := context.Background() |
| 22 | opt, data := t.TempDir(), t.TempDir() |
| 23 | store := release.NewDirStore(t.TempDir()) |
| 24 | seed, pubB64, _ := release.GenerateKey() |
| 25 | priv, _ := release.ParseSigningKey(seed, "t") |
| 26 | pub, _ := release.ParsePublicKey(pubB64, "t") |
| 27 | for _, v := range []string{"v1", "v2"} { |
| 28 | m := release.Manifest{Schema: 1, App: "km", Version: v, Kind: "binary", |
| 29 | Artifacts: map[string]release.Artifact{runtimePlatform(): {}}} |
| 30 | if err := release.Ship(ctx, store, "bab", m, map[string][]byte{runtimePlatform(): []byte(v)}); err != nil { |
| 31 | t.Fatal(err) |
| 32 | } |
| 33 | if err := release.Promote(ctx, store, priv, "bab", "km", "canary/x", v); err != nil { |
| 34 | t.Fatal(err) |
| 35 | } |
| 36 | } |
| 37 | reg, err := registry.Open(filepath.Join(t.TempDir(), "r.db")) |
| 38 | if err != nil { |
| 39 | t.Fatal(err) |
| 40 | } |
| 41 | pinned := release.UpdateSettings{Updates: release.UpdatesAuto, Channel: "canary/x", Pin: "v1"}.Column() |
| 42 | add := func(host string, good string) { |
| 43 | t.Helper() |
| 44 | if err := reg.Upsert(registry.Route{Host: host, Kind: "instance", App: "km", Account: "bab", Channel: "canary/x"}); err != nil { |
| 45 | t.Fatal(err) |
| 46 | } |
| 47 | if err := reg.SetUpdateSettings(host, pinned); err != nil { |
| 48 | t.Fatal(err) |
| 49 | } |
| 50 | if err := reg.SetBuildState(host, release.BuildStateCopy{Known: true}.Column()); err != nil { |
| 51 | t.Fatal(err) |
| 52 | } |
| 53 | if good != "" { |
| 54 | if err := reg.SetGoodBuild(host, registry.GoodBuild{}, registry.GoodBuild{Version: good}); err != nil { |
| 55 | t.Fatal(err) |
| 56 | } |
| 57 | } |
| 58 | } |
| 59 | opts := []release.PassOption{updatesPassOption(data, true, func(context.Context, release.Store, ed25519.PublicKey) (*release.RootUpdateInputs, error) { |
| 60 | return nil, nil |
| 61 | })} // built once |
| 62 | pass := func() { |
| 63 | t.Helper() |
| 64 | if err := release.PassFilter(ctx, reg, store, pub, opt, nil, nil, opts...); err != nil { |
| 65 | t.Fatal(err) |
| 66 | } |
| 67 | } |
| 68 | live := func(host string) string { |
| 69 | v, _ := release.LiveVersion(opt, host) |
| 70 | return v |
| 71 | } |
| 72 | |
| 73 | add("first.example.com", "") |
| 74 | pass() // no book yet: the pin installs |
| 75 | if v := live("first.example.com"); v != "v1" { |
| 76 | t.Fatalf("first live %q, want v1 with no failure records", v) |
| 77 | } |
| 78 | |
| 79 | // The agent records v1 failed after the option was built. |
| 80 | book := `{"schema":1,"records":[{"account":"bab","app":"km","build":"v1","host":"first.example.com", |
| 81 | "cause":"health","mode":"canary","reason":"did not come up", |
| 82 | "held_down":["held.example.com"],"retried":["retried.example.com"]}]}` |
| 83 | if err := os.WriteFile(filepath.Join(data, rollout.FailureFile), []byte(book), 0o644); err != nil { |
| 84 | t.Fatal(err) |
| 85 | } |
| 86 | add("blocked.example.com", "") // Blocks: not served, not retried |
| 87 | add("served.example.com", "v1") // served: Blocks exempts, not held down |
| 88 | add("held.example.com", "v1") // served, but held down on v1: HeldDown |
| 89 | add("retried.example.com", "") // a host retry cleared it |
| 90 | pass() |
| 91 | for host, want := range map[string]string{ |
| 92 | "blocked.example.com": "", |
| 93 | "served.example.com": "v1", |
| 94 | "held.example.com": "", |
| 95 | "retried.example.com": "v1", |
| 96 | } { |
| 97 | if v := live(host); v != want { |
| 98 | t.Fatalf("%s live %q, want %q", host, v, want) |
| 99 | } |
| 100 | } |
| 101 | for _, host := range []string{"blocked.example.com", "held.example.com"} { |
| 102 | c, err := release.ReadConsumed(opt, host) |
| 103 | if err != nil || c == nil || c.Reason != string(release.ReasonFailed) || c.Detail != "v1" { |
| 104 | t.Fatalf("%s consumed = %+v, %v; want reason failed naming v1", host, c, err) |
| 105 | } |
| 106 | } |
| 107 | } |
| 108 | |