package host import ( "context" "errors" "github.com/carlosframework/platform/internal/activator" "github.com/carlosframework/platform/internal/appconfig" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" "os" "path/filepath" "testing" ) type fix2IndexStore struct { release.Store lists int accountFailure bool } func (s *fix2IndexStore) List(ctx context.Context, prefix string) ([]string, error) { s.lists++ if prefix == "accounts/" && s.accountFailure { return []string{"accounts/bap/account.json"}, nil } return s.Store.List(ctx, prefix) } func (s *fix2IndexStore) Get(ctx context.Context, key string) ([]byte, string, error) { if key == "accounts/bap/account.json" && s.accountFailure { s.accountFailure = false return nil, "", os.ErrDeadlineExceeded } return s.Store.Get(ctx, key) } func TestPlan14Fix2RecordIndexCachesDiscoveryAndRefreshesBodies(t *testing.T) { e, rt, rec, source := plan14ProofFixture(t) store := &fix2IndexStore{Store: e.store} source.store = store source.cacheIndex = true if _, err := source.resolve(t.Context(), rt); err != nil { t.Fatal("record discovery failed", err) } lists := store.lists rec.RecordSeq++ plan14PutRecord(t, e.store, rec) first, err := source.resolve(t.Context(), rt) if err != nil { t.Fatal(err) } rec.RecordSeq++ plan14PutRecord(t, e.store, rec) second, err := source.resolve(t.Context(), rt) if err != nil { t.Fatal(err) } if first.RecordETag == second.RecordETag || store.lists != lists { t.Fatal("cached bodies or repeated listing", first.RecordETag, second.RecordETag, lists, store.lists) } store.accountFailure = true if _, _, err := provision.FindRecordByHost(t.Context(), store, rt.Host); !errors.Is(err, provision.ErrRecordIndexRetry) || errors.Is(err, os.ErrPermission) { t.Fatal("partial listing not retryable", err) } } func TestPlan14Fix2OrdinaryInstanceUnchanged(t *testing.T) { e, rt, rec, source := plan14ProofFixture(t) rec.Unit = "" rec.Addr = "" rec.Health = nil plan14PutRecord(t, e.store, rec) rt.Unit = "forged.service" if err := e.root.reg.Upsert(rt); err != nil { t.Fatal(err) } unit := activator.UnitName(rt.Host) file := filepath.Join(t.TempDir(), "carlos-app@.service") if err := os.WriteFile(file, []byte("root unit"), 0644); err != nil { t.Fatal(err) } u := &plan14OrdinaryUnits{} u.props = unitProps{Name: unit, ExecPath: release.LiveLink(source.optDir, rt.Host), FragmentPath: file, ActiveState: "active"} checks := rootRestartChecks{dispatch: source.needsRestart, targetFile: os.Open} restart := rootRestartRouteWith(e.root.reg, u, source.optDir, nil, nil, checks) if err := restart(rt.Host); err != nil { t.Fatal(err) } if len(u.effects) != 1 || u.effects[0] != unit { t.Fatal("ordinary unit not restarted", u.effects) } u.props.ActiveState = "inactive" if err := restart(rt.Host); err != nil { t.Fatal(err) } if len(u.effects) != 1 { t.Fatal("operator-stopped ordinary unit restarted") } } func TestPlan14Fix2OrdinaryAdoptionConfigAndRestartRequests(t *testing.T) { t.Parallel() e, rt, u, c, pub := plan14AdoptionFixture(t) if err := e.store.Delete(t.Context(), provision.RecordKey(rt.Account, rt.App, rt.Host)); err != nil { t.Fatal(err) } rt.Unit, rt.Addr, rt.Health = "", "", "" rt.Backing, rt.Socket = registry.BackingUnit, provision.DefaultSocket(rt.Host) if err := e.root.reg.Delete(rt.Host); err != nil { t.Fatal(err) } if err := e.root.reg.Upsert(rt); err != nil { t.Fatal(err) } u.props.Name = activator.UnitName(rt.Host) source := &proofBindingSource{store: e.store, sys: u, optDir: u.opt, openFile: os.Open, unitDirs: []string{filepath.Dir(u.props.FragmentPath)}, namedPath: filepath.Join(t.TempDir(), "absent.json")} c.dispatch = source.needsRestart restart := rootRestartRouteWith(e.root.reg, u, u.opt, nil, nil, c) if err := release.Pass(t.Context(), e.root.reg, e.store, pub, u.opt, restart, release.WithRestartFailurePropagation()); err != nil { t.Fatal("ordinary adoption", err) } if len(u.effects) != 1 || u.exe != release.VersionPath(u.opt, rt.Account, "v2", rt.App) { t.Fatal("ordinary adoption rolled back or missed restart", u.effects, u.exe) } if err := appconfig.SetEnv(t.Context(), e.store, rt.Account, rt.App, map[string]string{"LOG_LEVEL": "debug"}); err != nil { t.Fatal(err) } envDir := t.TempDir() for i := 0; i < 2; i++ { if err := appconfig.Sync(t.Context(), e.root.reg, e.store, nil, envDir, restart, nil); err != nil { t.Fatal("config sync", err) } } if len(u.effects) != 2 { t.Fatal("config restart missing or repeated", u.effects) } routes, err := e.root.reg.List() if err != nil { t.Fatal(err) } marks := filepath.Join(t.TempDir(), "restart-marks.json") restartPass(t.Context(), routes, e.store, marks, restart) askRestart(t, t.Context(), e.store, rt.Account, rt.App) restartPass(t.Context(), routes, e.store, marks, restart) restartPass(t.Context(), routes, e.store, marks, restart) if len(u.effects) != 3 { t.Fatal("restart request missing or repeated", u.effects) } }