| index e6e51a3e9..8cbaf7fed 100644 |
| --- a/internal/appconfig/instances_test.go |
| +++ b/internal/appconfig/instances_test.go |
| @@ -222,3 +222,155 @@ func TestWritingTheInstanceLayerBumpsTheGeneration(t *testing.T) { |
| t.Fatalf("second DeleteInstanceConfig: %v", err) |
| } |
| } |
| + |
| +// The precedence the design states: default < environment < instance |
| +// (§6.2). Three layers, each winning per key over the one below it, and the |
| +// instance layer's etag handed back so the caller can later check what was |
| +// delivered. |
| +func TestLoadMergedHostResolvesDefaultThenEnvironmentThenInstance(t *testing.T) { |
| + ctx := context.Background() |
| + s := release.NewDirStore(t.TempDir()) |
| + if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{ |
| + "FROM_DEFAULT": "default", "FROM_ENV": "default", "FROM_INSTANCE": "default"}); err != nil { |
| + t.Fatalf("SetEnv: %v", err) |
| + } |
| + if _, err := ApplyEnvironment(ctx, s, "bab", "titogo", "prod", map[string]Var{ |
| + "FROM_ENV": {Kind: "env", Value: "environment"}, |
| + "FROM_INSTANCE": {Kind: "env", Value: "environment"}}, nil); err != nil { |
| + t.Fatalf("ApplyEnvironment: %v", err) |
| + } |
| + _, want, err := ApplyInstance(ctx, s, "bab", "titogo", "book.customer.example.com", map[string]Var{ |
| + "FROM_INSTANCE": {Kind: "env", Value: "instance"}}, nil) |
| + if err != nil { |
| + t.Fatalf("ApplyInstance: %v", err) |
| + } |
| + |
| + m, etag, err := LoadMergedHost(ctx, s, "bab", "titogo", "prod", "book.customer.example.com") |
| + if err != nil { |
| + t.Fatalf("LoadMergedHost: %v", err) |
| + } |
| + if etag != want { |
| + t.Fatalf("etag = %q, want the overlay's own %q", etag, want) |
| + } |
| + for k, wantVal := range map[string]string{ |
| + "FROM_DEFAULT": "default", "FROM_ENV": "environment", "FROM_INSTANCE": "instance"} { |
| + if got := m.Vars[k].Value; got != wantVal { |
| + t.Errorf("%s = %q, want %q", k, got, wantVal) |
| + } |
| + } |
| + |
| + // A sibling with no overlay of its own gets the two-layer merge and an |
| + // empty etag, which is what says "this host has no overlay" rather than |
| + // "this agent did not render one". |
| + m2, etag2, err := LoadMergedHost(ctx, s, "bab", "titogo", "prod", "other.customer.example.com") |
| + if err != nil { |
| + t.Fatalf("LoadMergedHost (sibling): %v", err) |
| + } |
| + if etag2 != "" { |
| + t.Fatalf("a host with no overlay reported etag %q", etag2) |
| + } |
| + if m2.Vars["FROM_INSTANCE"].Value != "environment" { |
| + t.Fatalf("the sibling saw another instance's value: %+v", m2.Vars) |
| + } |
| +} |
| + |
| +// With no overlay, LoadMergedHost is LoadMerged, to the field. That matters |
| +// for the unbound case in particular: LoadMerged returns the default bundle |
| +// OBJECT there, exec_delivery and all, and a merge that quietly rebuilt it |
| +// would strip a flag other code reads off the same manifest. |
| +func TestLoadMergedHostWithNoOverlayIsExactlyLoadMerged(t *testing.T) { |
| + ctx := context.Background() |
| + s := release.NewDirStore(t.TempDir()) |
| + if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{"K": "v"}); err != nil { |
| + t.Fatalf("SetEnv: %v", err) |
| + } |
| + if err := SetExecDelivery(ctx, s, "bab", "titogo", ExecDeliveryFile); err != nil { |
| + t.Fatalf("SetExecDelivery: %v", err) |
| + } |
| + want, err := LoadMerged(ctx, s, "bab", "titogo", "") |
| + if err != nil { |
| + t.Fatalf("LoadMerged: %v", err) |
| + } |
| + got, etag, err := LoadMergedHost(ctx, s, "bab", "titogo", "", "book.customer.example.com") |
| + if err != nil || etag != "" { |
| + t.Fatalf("LoadMergedHost = %q %v", etag, err) |
| + } |
| + if got.ExecDelivery != want.ExecDelivery || got.Vars["K"] != want.Vars["K"] || len(got.Vars) != len(want.Vars) { |
| + t.Fatalf("LoadMergedHost diverged from LoadMerged with no overlay: %+v vs %+v", got, want) |
| + } |
| +} |
| + |
| +// exec_delivery is never read from this layer (the ruling at |
| +// appconfig.go:66-71). An overlay that carries the field does not put it in |
| +// the merged view, so nothing downstream can act on it. |
| +func TestInstanceLayerNeverCarriesExecDelivery(t *testing.T) { |
| + ctx := context.Background() |
| + s := release.NewDirStore(t.TempDir()) |
| + if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{"K": "v"}); err != nil { |
| + t.Fatalf("SetEnv: %v", err) |
| + } |
| + putInstanceRaw(t, ctx, s, "bab", "titogo", "book.customer.example.com", &Manifest{ |
| + Schema: 1, App: "titogo", Vars: map[string]Var{"OWN": {Kind: "env", Value: "1"}}, |
| + ExecDelivery: ExecDeliveryFile}) |
| + m, _, err := LoadMergedHost(ctx, s, "bab", "titogo", "", "book.customer.example.com") |
| + if err != nil { |
| + t.Fatalf("LoadMergedHost: %v", err) |
| + } |
| + if m.ExecDelivery != "" { |
| + t.Fatalf("exec_delivery = %q: the instance layer must never decide delivery", m.ExecDelivery) |
| + } |
| + if m.Vars["OWN"].Value != "1" { |
| + t.Fatalf("the overlay's vars were dropped along with its flag: %+v", m.Vars) |
| + } |
| +} |
| + |
| +// A route bound to a bundle that does not exist is still an ERROR, overlay |
| +// or no overlay. The binding asserted the bundle exists; an overlay does not |
| +// make that assertion true, and answering with a partial merge would hand a |
| +// live route a file built from a binding nobody honoured. |
| +func TestLoadMergedHostStillRefusesAMissingBoundBundle(t *testing.T) { |
| + ctx := context.Background() |
| + s := release.NewDirStore(t.TempDir()) |
| + if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{"K": "v"}); err != nil { |
| + t.Fatalf("SetEnv: %v", err) |
| + } |
| + if _, _, err := ApplyInstance(ctx, s, "bab", "titogo", "book.customer.example.com", |
| + map[string]Var{"OWN": {Kind: "env", Value: "1"}}, nil); err != nil { |
| + t.Fatalf("ApplyInstance: %v", err) |
| + } |
| + if _, _, err := LoadMergedHost(ctx, s, "bab", "titogo", "gone", "book.customer.example.com"); err == nil { |
| + t.Fatal("a missing bound bundle resolved anyway because the host had an overlay") |
| + } |
| +} |
| + |
| +// The index the sweeps use: one listing per app answers "which of this |
| +// app's instances have an overlay at all", so a pass costs a Get only for |
| +// the instances that do. |
| +func TestInstanceOverlaysListsOnlyThisAppsHosts(t *testing.T) { |
| + ctx := context.Background() |
| + s := release.NewDirStore(t.TempDir()) |
| + have, err := instanceOverlays(ctx, s, "bab", "titogo") |
| + if err != nil || len(have) != 0 { |
| + t.Fatalf("empty app = %v %v, want an empty set and no error", have, err) |
| + } |
| + for _, host := range []string{"book.customer.example.com", "cart.customer.example.com"} { |
| + if _, _, err := ApplyInstance(ctx, s, "bab", "titogo", host, |
| + map[string]Var{"OWN": {Kind: "env", Value: host}}, nil); err != nil { |
| + t.Fatalf("ApplyInstance %s: %v", host, err) |
| + } |
| + } |
| + if _, _, err := ApplyInstance(ctx, s, "bab", "eleven", "other.example.com", |
| + map[string]Var{"OWN": {Kind: "env", Value: "x"}}, nil); err != nil { |
| + t.Fatalf("ApplyInstance (other app): %v", err) |
| + } |
| + have, err = instanceOverlays(ctx, s, "bab", "titogo") |
| + if err != nil { |
| + t.Fatalf("instanceOverlays: %v", err) |
| + } |
| + if len(have) != 2 || !have["book.customer.example.com"] || !have["cart.customer.example.com"] { |
| + t.Fatalf("instanceOverlays = %v, want just this app's two hosts", have) |
| + } |
| + if have["other.example.com"] { |
| + t.Fatal("another app's overlay leaked into this app's index") |
| + } |
| +} |