| index 3ac51fb54..eeca7a231 100644 |
| --- a/internal/appconfig/instances_test.go |
| +++ b/internal/appconfig/instances_test.go |
| @@ -1051,3 +1051,131 @@ func TestSyncExecMassRemovalGuardHoldsRemovalsButNotOverlays(t *testing.T) { |
| t.Fatalf("the overlay did not converge under the guard:\n%s", book) |
| } |
| } |
| + |
| +// The layer's only convergence signal is the generation object. The route |
| +// fingerprint cannot see a bucket write and must not start paying to try: |
| +// this pins both halves of that at once. |
| +func TestWritingAnOverlayMovesTheGenerationAndNotTheFingerprint(t *testing.T) { |
| + ctx := context.Background() |
| + s := release.NewDirStore(t.TempDir()) |
| + if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{"APP_WIDE": "yes"}); err != nil { |
| + t.Fatalf("SetEnv: %v", err) |
| + } |
| + routes := []registry.Route{{ |
| + Host: "book.example.com", Kind: "instance", App: "titogo", Account: "bab", |
| + Status: "ready", Socket: "/run/carlos/book.sock", |
| + }} |
| + execBefore, unitBefore := ConfigRouteFingerprints(routes) |
| + _, genBefore, err := s.Get(ctx, GenKey("bab", "titogo")) |
| + if err != nil { |
| + t.Fatalf("read generation: %v", err) |
| + } |
| + |
| + if _, _, err := ApplyInstance(ctx, s, "bab", "titogo", "book.example.com", |
| + map[string]Var{"TENANT": {Kind: "env", Value: "book"}}, nil); err != nil { |
| + t.Fatalf("ApplyInstance: %v", err) |
| + } |
| + |
| + execAfter, unitAfter := ConfigRouteFingerprints(routes) |
| + if execAfter != execBefore || unitAfter != unitBefore { |
| + t.Fatal("the route fingerprint moved for a bucket-side write, which would cost a read per route per tick") |
| + } |
| + _, genAfter, err := s.Get(ctx, GenKey("bab", "titogo")) |
| + if err != nil { |
| + t.Fatalf("read generation: %v", err) |
| + } |
| + if genAfter == genBefore { |
| + t.Fatal("the generation did not move, so the fast loop has nothing to notice and delivery waits out the hourly floor") |
| + } |
| +} |
| + |
| +// One pass over a box holding every shape this layer comes in: an instance |
| +// with an overlay over its app's bundle, a sibling with none, and an |
| +// instance whose app has no bundle at all so its overlay stands alone. |
| +// Then the overlay is removed and the route converges back, with the ack |
| +// saying so. |
| +func TestOnePassOverEveryInstanceLayerShape(t *testing.T) { |
| + ctx, s, reg, dd, privB64, _ := execKit(t) |
| + priv, err := ParsePrivateKey(privB64, "test") |
| + if err != nil { |
| + t.Fatalf("ParsePrivateKey: %v", err) |
| + } |
| + if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{"APP_WIDE": "yes", "TENANT": "none"}); err != nil { |
| + t.Fatalf("SetEnv: %v", err) |
| + } |
| + _, bookETag, err := ApplyInstance(ctx, s, "bab", "titogo", "book.example.com", |
| + map[string]Var{"TENANT": {Kind: "env", Value: "book"}}, nil) |
| + if err != nil { |
| + t.Fatalf("ApplyInstance (book): %v", err) |
| + } |
| + _, deskETag, err := ApplyInstance(ctx, s, "bab", "solo", "desk.example.com", |
| + map[string]Var{"TENANT": {Kind: "env", Value: "desk"}}, nil) |
| + if err != nil { |
| + t.Fatalf("ApplyInstance (desk): %v", err) |
| + } |
| + for host, app := range map[string]string{ |
| + "book.example.com": "titogo", "cart.example.com": "titogo", "desk.example.com": "solo"} { |
| + if err := reg.Upsert(registry.Route{ |
| + Host: host, Kind: "instance", App: app, Account: "bab", Status: "ready", |
| + Socket: "/run/carlos/" + host + ".sock", |
| + }); err != nil { |
| + t.Fatalf("Upsert %s: %v", host, err) |
| + } |
| + } |
| + |
| + acks := map[string]ConfigAck{} |
| + collect := func(a ConfigAck) { acks[a.Host] = a } |
| + if err := SyncExec(ctx, reg, s, priv, dd, func(string) error { return nil }, "", collect); err != nil { |
| + t.Fatalf("SyncExec: %v", err) |
| + } |
| + read := func(host string) string { |
| + t.Helper() |
| + body, err := os.ReadFile(filepath.Join(dd, host+".config.env")) |
| + if err != nil { |
| + t.Fatalf("read %s: %v", host, err) |
| + } |
| + return string(body) |
| + } |
| + if got := read("book.example.com"); !strings.Contains(got, "TENANT=book\n") || !strings.Contains(got, "APP_WIDE=yes\n") { |
| + t.Fatalf("book: %q", got) |
| + } |
| + if got := read("cart.example.com"); !strings.Contains(got, "TENANT=none\n") { |
| + t.Fatalf("cart saw another instance's config: %q", got) |
| + } |
| + if got := read("desk.example.com"); got != "TENANT=desk\n" { |
| + t.Fatalf("desk, whose app has no bundle, should render its overlay alone: %q", got) |
| + } |
| + for host, want := range map[string]string{ |
| + "book.example.com": bookETag, "cart.example.com": "", "desk.example.com": deskETag} { |
| + a, ok := acks[host] |
| + if !ok { |
| + t.Fatalf("no ack for %s", host) |
| + } |
| + if a.Schema != ConfigAckSchema || a.InstanceConfigETag != want { |
| + t.Fatalf("%s ack = %+v, want schema %d and etag %q", host, a, ConfigAckSchema, want) |
| + } |
| + if v := InstanceConfigDelivery(&a, want); v != InstanceConfigDelivered { |
| + t.Fatalf("%s delivery = %q, want %q", host, v, InstanceConfigDelivered) |
| + } |
| + } |
| + |
| + // Remove one overlay: that route converges back to its app's merge, and |
| + // its ack stops naming an overlay, which is what tells a reader the |
| + // removal actually reached the box. |
| + if err := DeleteInstanceConfig(ctx, s, "bab", "titogo", "book.example.com"); err != nil { |
| + t.Fatalf("DeleteInstanceConfig: %v", err) |
| + } |
| + acks = map[string]ConfigAck{} |
| + if err := SyncExec(ctx, reg, s, priv, dd, func(string) error { return nil }, "", collect); err != nil { |
| + t.Fatalf("SyncExec (after delete): %v", err) |
| + } |
| + if got := read("book.example.com"); !strings.Contains(got, "TENANT=none\n") { |
| + t.Fatalf("book did not converge back to its app's merge: %q", got) |
| + } |
| + if a := acks["book.example.com"]; a.InstanceConfigETag != "" || a.Schema != ConfigAckSchema { |
| + t.Fatalf("book ack after the delete = %+v, want the schema and no overlay", a) |
| + } |
| + if got := read("desk.example.com"); got != "TENANT=desk\n" { |
| + t.Fatalf("desk was disturbed by a sibling app's delete: %q", got) |
| + } |
| +} |