package appconfig import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "os" "testing" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) const ( heldOp = "mv_abcdefghijklmnopqrstuvwxyz" heldBox = "uk-box" heldHost = "held.example.com" ) // heldFixture writes all three config layers for bab/eleven and returns the // etag the dir store reports for each object. func heldFixture(t *testing.T, ctx context.Context, s release.Store) (def, env, overlay string) { t.Helper() if err := SetEnv(ctx, s, "bab", "eleven", map[string]string{"FROM_DEFAULT": "d", "SHARED": "default"}); err != nil { t.Fatalf("SetEnv: %v", err) } if _, err := ApplyEnvironment(ctx, s, "bab", "eleven", "prod", map[string]Var{"SHARED": {Kind: "env", Value: "environment"}}, nil); err != nil { t.Fatalf("ApplyEnvironment: %v", err) } if _, _, err := ApplyInstance(ctx, s, "bab", "eleven", heldHost, map[string]Var{"FROM_INSTANCE": {Kind: "env", Value: "i"}}, nil); err != nil { t.Fatalf("ApplyInstance: %v", err) } etagOf := func(key string) string { _, etag, err := s.Get(ctx, key) if err != nil { t.Fatalf("Get %s: %v", key, err) } return etag } return etagOf(Key("bab", "eleven")), etagOf(EnvironmentKey("bab", "eleven", "prod")), etagOf(InstanceConfigKey("bab", "eleven", heldHost)) } func landingRow() registry.Route { return registry.Route{Host: heldHost, Kind: "instance", App: "eleven", Account: "bab", Environment: "prod", Status: "landing", HoldOp: heldOp, Socket: "/run/carlos/held.sock"} } // TestLoadMergedHostVersionsReportsTheMergedObjects: the etag of every // object the merge actually read comes back with the manifest, and an // absent layer reports "". func TestLoadMergedHostVersionsReportsTheMergedObjects(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) def, env, overlay := heldFixture(t, ctx, s) m, v, err := LoadMergedHostVersions(ctx, s, "bab", "eleven", "prod", heldHost) if err != nil { t.Fatalf("LoadMergedHostVersions: %v", err) } if v != (MergedVersions{Default: def, Environment: env, Overlay: overlay}) { t.Fatalf("versions = %+v, want default %q environment %q overlay %q", v, def, env, overlay) } for k, want := range map[string]string{"FROM_DEFAULT": "d", "SHARED": "environment", "FROM_INSTANCE": "i"} { if got := m.Vars[k].Value; got != want { t.Errorf("%s = %q, want %q", k, got, want) } } legacy, etag, err := LoadMergedHost(ctx, s, "bab", "eleven", "prod", heldHost) if err != nil || etag != overlay || len(legacy.Vars) != len(m.Vars) { t.Fatalf("LoadMergedHost = %d vars, %q, %v; want the same merge and the overlay etag %q", len(legacy.Vars), etag, err, overlay) } // Unbound, no overlay: only the default is read, the other two are "". _, v2, err := LoadMergedHostVersions(ctx, s, "bab", "eleven", "", "other.example.com") if err != nil { t.Fatalf("LoadMergedHostVersions (unbound): %v", err) } if v2 != (MergedVersions{Default: def}) { t.Fatalf("unbound versions = %+v, want only the default %q", v2, def) } } func readAck(t *testing.T, ctx context.Context, s release.Store) ConfigAck { t.Helper() raw, _, err := s.Get(ctx, ConfigAckKey(heldBox, heldHost)) if err != nil { t.Fatalf("ack: %v", err) } var a ConfigAck if err := json.Unmarshal(raw, &a); err != nil { t.Fatal(err) } return a } // TestRenderHeldWritesALandingRowSyncExecSkips: SyncExec leaves a landing // row alone (execRouteBase), RenderHeld renders it through the same writer // and acks it bound to the move: the op, the file's sha256 and the etags of // the default and environment objects the dir store reports. func TestRenderHeldWritesALandingRowSyncExecSkips(t *testing.T) { ctx, s, reg, dd, privB64, _ := execKit(t) priv, err := ParsePrivateKey(privB64, "test") if err != nil { t.Fatal(err) } def, env, overlay := heldFixture(t, ctx, s) rt := landingRow() if err := reg.Upsert(rt); err != nil { t.Fatal(err) } if err := SyncExec(ctx, reg, s, priv, dd, nil, "", nil); err != nil { t.Fatalf("SyncExec: %v", err) } path := ConfigEnvPath(dd, rt) if _, err := os.Stat(path); !os.IsNotExist(err) { t.Fatalf("SyncExec wrote a landing row's file: %v", err) } got, err := RenderHeld(ctx, s, priv, dd, rt, heldOp, heldBox) if err != nil { t.Fatalf("RenderHeld: %v", err) } body, err := os.ReadFile(path) if err != nil { t.Fatalf("RenderHeld wrote no file: %v", err) } sum := sha256.Sum256(body) wantSHA := hex.EncodeToString(sum[:]) wantTags := map[string]string{"default": def, "environment": env} if got.Path != path || got.SHA256 != wantSHA || got.OverlayETag != overlay || len(got.BundleETags) != 2 || got.BundleETags["default"] != def || got.BundleETags["environment"] != env { t.Fatalf("HeldRender = %+v, want path %s sha %s bundles %v overlay %q", got, path, wantSHA, wantTags, overlay) } m, _, err := LoadMergedHostVersions(ctx, s, "bab", "eleven", "prod", heldHost) if err != nil { t.Fatal(err) } want, err := RenderEnv(m, priv, routeBinding(rt)) if err != nil { t.Fatal(err) } if string(body) != string(want) { t.Fatalf("file = %q, want the three-layer render %q", body, want) } a := readAck(t, ctx, s) if a.MoveOp != heldOp || a.FileSHA256 != wantSHA || a.Box != heldBox || a.Host != heldHost || a.InstanceConfigETag != overlay || a.Schema != ConfigAckSchema || a.Shape != "socket-exec" { t.Fatalf("ack = %+v", a) } if len(a.BundleETags) != 2 || a.BundleETags["default"] != def || a.BundleETags["environment"] != env { t.Fatalf("ack bundle etags = %v, want %v", a.BundleETags, wantTags) } } // TestRenderHeldAcksASecondCallWithNoChange: nothing changed, so the file // is not rewritten, but the ack is: T3 reads it for this round. func TestRenderHeldAcksASecondCallWithNoChange(t *testing.T) { ctx, s, _, dd, privB64, _ := execKit(t) priv, err := ParsePrivateKey(privB64, "test") if err != nil { t.Fatal(err) } heldFixture(t, ctx, s) rt := landingRow() first, err := RenderHeld(ctx, s, priv, dd, rt, heldOp, heldBox) if err != nil { t.Fatalf("RenderHeld: %v", err) } if err := s.Delete(ctx, ConfigAckKey(heldBox, heldHost)); err != nil { t.Fatal(err) } second, err := RenderHeld(ctx, s, priv, dd, rt, heldOp, heldBox) if err != nil { t.Fatalf("second RenderHeld: %v", err) } if second.SHA256 != first.SHA256 || second.Path != first.Path { t.Fatalf("second = %+v, first = %+v", second, first) } if a := readAck(t, ctx, s); a.MoveOp != heldOp || a.FileSHA256 != first.SHA256 { t.Fatalf("second call's ack = %+v", a) } } // TestRenderHeldRefusesWhileHostClaimIsWired: ruling F10. RenderHeld writes // through legacyHostWork, so with CARLOS_HOST_CLAIM on it refuses and // writes nothing, neither file nor ack. Plan 2 adds the held-row selection. func TestRenderHeldRefusesWhileHostClaimIsWired(t *testing.T) { ctx, s, _, dd, privB64, _ := execKit(t) priv, err := ParsePrivateKey(privB64, "test") if err != nil { t.Fatal(err) } heldFixture(t, ctx, s) t.Setenv("CARLOS_HOST_CLAIM", "1") rt := landingRow() if _, err := RenderHeld(ctx, s, priv, dd, rt, heldOp, heldBox); !errors.Is(err, ErrHeldClaimWired) { t.Fatalf("RenderHeld with the claim wired: %v, want ErrHeldClaimWired", err) } if _, err := os.Stat(ConfigEnvPath(dd, rt)); !os.IsNotExist(err) { t.Fatalf("a file was written: %v", err) } if _, _, err := s.Get(ctx, ConfigAckKey(heldBox, heldHost)); !errors.Is(err, os.ErrNotExist) { t.Fatalf("an ack was written: %v", err) } } // TestRenderHeldRefusesWhatItDoesNotOwn: a row SyncExec owns (ready, or // no status at all) would race SyncExec on one file, a row held by another // operation is not this move's, and a missing op or box cannot be acked. func TestRenderHeldRefusesWhatItDoesNotOwn(t *testing.T) { ctx, s, _, dd, privB64, _ := execKit(t) priv, err := ParsePrivateKey(privB64, "test") if err != nil { t.Fatal(err) } heldFixture(t, ctx, s) for name, tc := range map[string]struct { mutate func(*registry.Route) op, box string }{ "ready row": {func(r *registry.Route) { r.Status = "ready" }, heldOp, heldBox}, "statusless row": {func(r *registry.Route) { r.Status = "" }, heldOp, heldBox}, "held by another op": {func(r *registry.Route) { r.HoldOp = "mv_bcdefghijklmnopqrstuvwxyza" }, heldOp, heldBox}, "no op": {func(*registry.Route) {}, "", heldBox}, "no box": {func(*registry.Route) {}, heldOp, ""}, "unit-backed": {func(r *registry.Route) { r.Backing = registry.BackingUnit }, heldOp, heldBox}, "no socket": {func(r *registry.Route) { r.Socket = "" }, heldOp, heldBox}, "not a bare host name": {func(r *registry.Route) { r.Host = "../x" }, heldOp, heldBox}, } { t.Run(name, func(t *testing.T) { rt := landingRow() tc.mutate(&rt) if _, err := RenderHeld(ctx, s, priv, dd, rt, tc.op, tc.box); !errors.Is(err, ErrNotHeld) { t.Fatalf("RenderHeld: %v, want ErrNotHeld", err) } if _, err := os.Stat(ConfigEnvPath(dd, landingRow())); !os.IsNotExist(err) { t.Fatalf("a file was written: %v", err) } }) } }