Skip to content

Add Init Hooks: Auto-Apply Seed Fixtures on Boot (#335 P1) - #339

Open
thzgajendra wants to merge 1 commit into
stackshy:developmentfrom
thzgajendra:feat/init-hooks
Open

Add Init Hooks: Auto-Apply Seed Fixtures on Boot (#335 P1)#339
thzgajendra wants to merge 1 commit into
stackshy:developmentfrom
thzgajendra:feat/init-hooks

Conversation

@thzgajendra

Copy link
Copy Markdown
Collaborator

Objective / Issue

P1 item of the "minikube for cloud resources" roadmap (#335). Bring the emulator up in a known state without manual seeding: drop *.json seed fixtures in an init directory and they're applied on every startup (docker-entrypoint.d-style).

What we found

  • seed.Load/seed.Apply already turn a declarative fixture into resources through the provider-agnostic driver interfaces, and serve already has a clean boot sequence (build providers → restore persistence → serve). So init hooks are a thin layer: read a directory of fixtures at the right point in boot.
  • Blast radius: additive. With no --init-dir (and no init.d), behavior is unchanged.

How we fixed it / How it works

  • serve --init-dir <dir>: after providers are built and any persisted state is restored, before serving, apply every *.json (lexical order) as a seed.Fixtures to all running providers. Fixtures are provider-agnostic, so one file seeds S3/Blob/GCS alike.
  • cloudemu start auto-loads <run-dir>/init.d when it exists (drop-in), unless the user passes an explicit --init-dir.
  • Failure policy: a malformed fixture fails startup (clear misconfiguration); a per-provider apply error — e.g. a resource that already exists from restored persistence — logs a warning and boot continues, so init can't wedge the stop→start path.
sequenceDiagram
    actor U as You
    participant S as serve (boot)
    participant D as init dir
    participant P as providers
    U->>D: drop 01-baseline.json (buckets/tables/secrets)
    U->>S: cloudemu start
    S->>S: build providers, restore persistence
    S->>D: read *.json (lexical order)
    S->>P: seed.Apply each fixture to every provider
    S-->>U: serving, already in the boot state
Loading

Alternatives not taken

  • Executable boot scripts (*.sh run post-ready with endpoint env) — the powerful but exec/security-heavy half of init hooks; deferred to a follow-up to keep this a small, safe change. Noted in the docs.
  • Apply to a single/primary provider — chose all-providers so one fixture yields a consistent baseline across clouds, matching the provider-agnostic fixture model.

Docs / Tests / Playground

  • Docs: docs/standalone-server.md — init-hooks section (drop-in usage, ordering, failure policy, scripts-deferred note).
  • Unit: applies fixtures in lexical order + ignores non-JSON; missing dir → no-op; malformed JSON → fails; duplicate (already-exists) → warns not fails.

Test plan

  • go test -race ./cmd/cloudemu/...
  • Full local CI: gofmt / build / vet / go test ./... / go mod tidy / golangci-lint = clean (CodeQL: only pre-existing findings, none in changed files).
  • Live E2E via cloudemu start + real aws CLI on the default ~/.cloudemu: dropped init.d/01-baseline.json (bucket+object, table+item, secret) → start → emulator booted straight into that state (config.yaml = env: local, DynamoDB item Ada, secret s3cr3t) with no manual seeding; restart re-applies cleanly.

Risk & Rollback

  • Low: additive and opt-in (no --init-dir / no init.d = unchanged). Malformed fixtures fail fast with a clear error; apply errors are non-fatal warnings.

Conclusion

cloudemu start now brings the local cloud up pre-seeded from a drop-in init.d, reusing the existing fixture engine.

Follow-up (#335): executable boot scripts (post-ready, endpoint env) for setup beyond the fixture schema.

Bring the emulator up in a known state without manual seeding: drop *.json seed
fixtures in an init directory and they're applied on every startup.

- serve --init-dir <dir>: after providers are built and any persisted state is
  restored, before serving, apply every *.json (lexical order) as a seed
  fixture to all running providers. A parse error fails startup; a per-provider
  apply error (e.g. a resource that already exists from restored state) logs a
  warning and boot continues.
- lifecycle start auto-loads <run-dir>/init.d when it exists (drop-in), unless
  the user passes an explicit --init-dir.

Fixtures are provider-agnostic, so one file seeds S3/Blob/GCS alike. Running
setup scripts on boot is a planned follow-up.

@NitinKumar004 NitinKumar004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deep review — init hooks

Small, clean, well-placed feature. Verified in an isolated worktree at the PR head; gates green (build + GOOS=windows go build ./cmd/cloudemu / vet / go test -race ./cmd/cloudemu/ ok / gofmt clean). Applied at the right point (after build + persistence-restore, before serving), iterates all providers per the provider-agnostic fixture model, no concurrency concern (boot is single-threaded before Serve), and start auto-loads <run-dir>/init.d unless --init-dir is given.

Holding on one Medium (silent-correctness) plus a Low note.

M1 (Medium) — a colliding resource silently truncates the rest of a fixture

applyInitFile warns-and-continues per file, but seed.Apply is category-level fail-fast: applyBuckets returns on the first CreateBucket error, and Apply then skips all tables/secrets/instances. So the first already-exists resource aborts everything after it in that fixture, while boot proceeds reporting only a warning. Two realistic silent-loss paths:

  • Two overlapping init files, no --persist: 00-base.json creates bucket X; 01-app.json has bucket X (dup) + table YY is never created, boot looks fine.
  • --persist + init.d overlap: restored state collides with a fixture's early resource → any new resource later in that same file is silently dropped on restart.

The "duplicate → warns not fails" policy (and TestApplyInitDirDuplicateWarnsNotFails, which has a single bucket) implies graceful per-resource skipping that isn't there — a duplicate truncates the fixture from that point. Fix: apply init idempotently at resource granularity (skip AlreadyExists and keep going — e.g. a best-effort / IgnoreExisting mode on seed.Apply, or pre-filter existing resources). At minimum, document that a fixture aborts at the first existing resource, and add a test asserting a post-collision resource still applies. Inline below.

L1 (Low) — gosec G703 on os.Stat(path) (lifecycle.go:77)

Taint from the --home-derived path — same linter-version drift as the earlier lifecycle PRs (newer bundled gosec; your --new-from-rev reports 0), and it's the user's own --home, not a trust boundary. Non-blocking; a //nolint:gosec closes it if the pinned linter ever flags it.

No AI attribution. Requesting changes on M1; L1 is a non-blocking note.

Comment thread cmd/cloudemu/serve.go
}

for prov, t := range targets {
if err := seed.Apply(ctx, f, t); err != nil {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

M1 (Medium). seed.Apply is category-level fail-fast — applyBuckets returns on the first CreateBucket that errors (e.g. AlreadyExists), and Apply then never reaches tables/secrets/instances. Since this loop only warns per provider/file, the first colliding resource silently drops every resource after it in the fixture, yet boot continues "successfully." Triggers without any exotic setup: two init files that overlap on an early resource (bucket X in 00, then X+table Y in 01Y lost), or --persist restoring a resource that an init fixture re-declares before a new one. Make init application idempotent at resource granularity — skip AlreadyExists and continue (a best-effort/IgnoreExisting mode on seed.Apply, or pre-filter what already exists) — so a duplicate skips just that resource, matching the documented "warn and continue." Please also extend TestApplyInitDirDuplicateWarnsNotFails with a second resource after the duplicate and assert it still gets created.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants