Repository navigation
feat(stack): allow repeatable --stack-id in stack destroy (#6964) #7000
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,6 +8,7 @@ import { CommandSettings } from "../../../../config/command-settings.service.ts" | |||||||||||||||
| import { TelemetryState } from "../../../../telemetry/telemetry-state.service.ts"; | ||||||||||||||||
| import { | ||||||||||||||||
| StackApi, | ||||||||||||||||
| type StackTarget, | ||||||||||||||||
| StackTargetError, | ||||||||||||||||
| rejectStackOutput, | ||||||||||||||||
| skippedRuntimeCleanupWarning, | ||||||||||||||||
|
|
@@ -32,8 +33,13 @@ const destroyError = (cause: StackError) => | |||||||||||||||
| cause, | ||||||||||||||||
| }); | ||||||||||||||||
|
|
||||||||||||||||
| export interface StackDestroyInput { | ||||||||||||||||
| readonly stack?: Option.Option<string> | string; | ||||||||||||||||
| readonly stackId?: ReadonlyArray<string> | Option.Option<string> | string; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| export const stackDestroy = Effect.fn("experimental.stack.destroy")(function* ( | ||||||||||||||||
| flags: StackDestroyFlags, | ||||||||||||||||
| flags: StackDestroyFlags | StackDestroyInput, | ||||||||||||||||
| ) { | ||||||||||||||||
| const telemetryState = yield* TelemetryState; | ||||||||||||||||
| const body = Effect.gen(function* () { | ||||||||||||||||
|
|
@@ -42,29 +48,74 @@ export const stackDestroy = Effect.fn("experimental.stack.destroy")(function* ( | |||||||||||||||
| const api = yield* StackApi; | ||||||||||||||||
| const outputFlag = yield* Effect.serviceOption(OutputFlag); | ||||||||||||||||
| yield* rejectStackOutput(outputFlag).pipe(Effect.mapError(mapTargetError)); | ||||||||||||||||
|
|
||||||||||||||||
| const stackOption = | ||||||||||||||||
| flags.stack !== undefined && Option.isOption(flags.stack) | ||||||||||||||||
| ? flags.stack | ||||||||||||||||
| : typeof flags.stack === "string" | ||||||||||||||||
| ? Option.some(flags.stack) | ||||||||||||||||
| : Option.none<string>(); | ||||||||||||||||
|
|
||||||||||||||||
| const rawStackId = flags.stackId; | ||||||||||||||||
| const stackIds: ReadonlyArray<string> = | ||||||||||||||||
| rawStackId === undefined | ||||||||||||||||
| ? [] | ||||||||||||||||
| : Array.isArray(rawStackId) | ||||||||||||||||
| ? rawStackId | ||||||||||||||||
| : typeof rawStackId === "string" | ||||||||||||||||
| ? [rawStackId] | ||||||||||||||||
| : Option.isOption(rawStackId) && Option.isSome(rawStackId) | ||||||||||||||||
| ? [rawStackId.value] | ||||||||||||||||
| : []; | ||||||||||||||||
|
|
||||||||||||||||
| yield* validateStackTarget({ | ||||||||||||||||
| stack: Option.getOrUndefined(flags.stack), | ||||||||||||||||
| stackId: Option.getOrUndefined(flags.stackId), | ||||||||||||||||
| stack: Option.getOrUndefined(stackOption), | ||||||||||||||||
| stackId: stackIds, | ||||||||||||||||
| }).pipe(Effect.mapError(mapTargetError)); | ||||||||||||||||
|
|
||||||||||||||||
| const resolver = yield* StackTargetResolver; | ||||||||||||||||
| const path = yield* Path.Path; | ||||||||||||||||
| const target = yield* resolver | ||||||||||||||||
| .resolve({ | ||||||||||||||||
| projectRoot: settings.workdir, | ||||||||||||||||
| ...(Option.isSome(flags.stack) ? { name: flags.stack.value } : {}), | ||||||||||||||||
| ...(Option.isSome(flags.stackId) ? { id: flags.stackId.value } : {}), | ||||||||||||||||
| runtime: "auto", | ||||||||||||||||
| }) | ||||||||||||||||
| .pipe(Effect.mapError(mapTargetError)); | ||||||||||||||||
| if (target.id === undefined) | ||||||||||||||||
| return yield* new StackCommandDestroyError({ | ||||||||||||||||
| reason: "flags", | ||||||||||||||||
| message: Option.isSome(flags.stack) | ||||||||||||||||
| ? `No managed stack named "${flags.stack.value}" was found for this project.` | ||||||||||||||||
| : "No managed stack was found for this project.", | ||||||||||||||||
| suggestion: "Choose an existing --stack name or --stack-id.", | ||||||||||||||||
| }); | ||||||||||||||||
|
|
||||||||||||||||
| const targets: StackTarget[] = []; | ||||||||||||||||
| if (stackIds.length > 0) { | ||||||||||||||||
| for (const id of stackIds) { | ||||||||||||||||
| const target = yield* resolver | ||||||||||||||||
| .resolve({ | ||||||||||||||||
| projectRoot: settings.workdir, | ||||||||||||||||
| id, | ||||||||||||||||
| runtime: "auto", | ||||||||||||||||
| }) | ||||||||||||||||
| .pipe(Effect.mapError(mapTargetError)); | ||||||||||||||||
| targets.push(target); | ||||||||||||||||
| } | ||||||||||||||||
| } else { | ||||||||||||||||
| const target = yield* resolver | ||||||||||||||||
| .resolve({ | ||||||||||||||||
| projectRoot: settings.workdir, | ||||||||||||||||
| ...(Option.isSome(stackOption) ? { name: stackOption.value } : {}), | ||||||||||||||||
| runtime: "auto", | ||||||||||||||||
| }) | ||||||||||||||||
| .pipe(Effect.mapError(mapTargetError)); | ||||||||||||||||
| if (target.id === undefined) | ||||||||||||||||
| return yield* new StackCommandDestroyError({ | ||||||||||||||||
| reason: "flags", | ||||||||||||||||
| message: Option.isSome(stackOption) | ||||||||||||||||
| ? `No managed stack named "${stackOption.value}" was found for this project.` | ||||||||||||||||
| : "No managed stack was found for this project.", | ||||||||||||||||
| suggestion: "Choose an existing --stack name or --stack-id.", | ||||||||||||||||
| }); | ||||||||||||||||
| targets.push(target); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| const seen = new Set<string>(); | ||||||||||||||||
| const uniqueTargets: StackTarget[] = []; | ||||||||||||||||
| for (const target of targets) { | ||||||||||||||||
| if (target.id !== undefined && !seen.has(target.id)) { | ||||||||||||||||
| seen.add(target.id); | ||||||||||||||||
| uniqueTargets.push(target); | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| const yes = yield* resolveYes; | ||||||||||||||||
| const tty = yield* Tty; | ||||||||||||||||
| if (!yes && (!tty.stdinIsTty || !output.interactive || output.format !== "text")) | ||||||||||||||||
|
|
@@ -73,8 +124,13 @@ export const stackDestroy = Effect.fn("experimental.stack.destroy")(function* ( | |||||||||||||||
| message: "Destroying a stack requires confirmation; rerun with --yes.", | ||||||||||||||||
| suggestion: "Pass --yes when running non-interactively or in a machine-readable format.", | ||||||||||||||||
| }); | ||||||||||||||||
| const scope = `stack ${target.id} at ${target.projectRoot} and its owned data`; | ||||||||||||||||
|
|
||||||||||||||||
| const preserved = "Storage upload files will be preserved."; | ||||||||||||||||
| const scope = | ||||||||||||||||
| uniqueTargets.length === 1 | ||||||||||||||||
| ? `stack ${uniqueTargets[0]!.id} at ${uniqueTargets[0]!.projectRoot} and its owned data` | ||||||||||||||||
| : `stacks ${uniqueTargets.map((t) => t.id).join(", ")} and their owned data`; | ||||||||||||||||
|
|
||||||||||||||||
| if (yes) yield* output.raw(`Permanently destroying ${scope}. ${preserved}\n`, "stderr"); | ||||||||||||||||
| else { | ||||||||||||||||
| const confirmed = yield* promptYesNo( | ||||||||||||||||
|
|
@@ -89,38 +145,113 @@ export const stackDestroy = Effect.fn("experimental.stack.destroy")(function* ( | |||||||||||||||
| message: "Stack destruction was not confirmed.", | ||||||||||||||||
| }); | ||||||||||||||||
| } | ||||||||||||||||
| const stack = yield* api | ||||||||||||||||
| .open({ | ||||||||||||||||
| id: target.id, | ||||||||||||||||
| stateRoot: path.join(settings.supabaseHome, "stacks"), | ||||||||||||||||
| cacheRoot: path.join(settings.supabaseHome, "cache", "stack"), | ||||||||||||||||
| }) | ||||||||||||||||
| .pipe(Effect.mapError(destroyError)); | ||||||||||||||||
| const destroying = yield* output.task(`Destroying stack ${target.id}...`); | ||||||||||||||||
| const result = yield* stack.destroy.pipe( | ||||||||||||||||
| Effect.onExit((exit) => | ||||||||||||||||
| Exit.isSuccess(exit) | ||||||||||||||||
| ? destroying.clear | ||||||||||||||||
| : Cause.hasInterruptsOnly(exit.cause) | ||||||||||||||||
| ? destroying.cancel() | ||||||||||||||||
| : destroying.fail(Option.getOrUndefined(Exit.findErrorOption(exit))?.message), | ||||||||||||||||
| ), | ||||||||||||||||
| Effect.mapError(destroyError), | ||||||||||||||||
| ); | ||||||||||||||||
| yield* Effect.annotateCurrentSpan({ | ||||||||||||||||
| "stack.prompted": !yes, | ||||||||||||||||
| "stack.runtime_cleanup": result.runtimeCleanup, | ||||||||||||||||
| }); | ||||||||||||||||
| if (result.runtimeCleanup === "skipped") | ||||||||||||||||
| yield* output.warn(skippedRuntimeCleanupWarning(`stack ${target.id}`, result)); | ||||||||||||||||
| if (output.format !== "text") | ||||||||||||||||
| yield* output.success("", { destroyed: true, id: target.id, ...result }); | ||||||||||||||||
| else if (result.runtimeCleanup === "complete") | ||||||||||||||||
| yield* output.raw(`Stack ${target.id} destroyed.\n`); | ||||||||||||||||
| else | ||||||||||||||||
| yield* output.raw( | ||||||||||||||||
| `Stack ${target.id} was removed locally; its ${result.engine === "docker" ? "Docker" : "Podman"} resources remain until the commands above are run.\n`, | ||||||||||||||||
| ); | ||||||||||||||||
|
|
||||||||||||||||
| const successes: Array<{ | ||||||||||||||||
| readonly id: string; | ||||||||||||||||
| readonly result: { | ||||||||||||||||
| readonly runtimeCleanup: "complete" | "partial" | "skipped"; | ||||||||||||||||
| readonly engine?: "docker" | "podman"; | ||||||||||||||||
| readonly remainingCommands?: ReadonlyArray<string>; | ||||||||||||||||
| }; | ||||||||||||||||
| }> = []; | ||||||||||||||||
| const failures: Array<{ | ||||||||||||||||
| readonly id: string; | ||||||||||||||||
| readonly error: StackCommandDestroyError; | ||||||||||||||||
| }> = []; | ||||||||||||||||
|
|
||||||||||||||||
| for (const target of uniqueTargets) { | ||||||||||||||||
| const targetId = target.id!; | ||||||||||||||||
| const destroySingle = Effect.gen(function* () { | ||||||||||||||||
| const stack = yield* api | ||||||||||||||||
| .open({ | ||||||||||||||||
| id: targetId, | ||||||||||||||||
| stateRoot: path.join(settings.supabaseHome, "stacks"), | ||||||||||||||||
| cacheRoot: path.join(settings.supabaseHome, "cache", "stack"), | ||||||||||||||||
| }) | ||||||||||||||||
| .pipe(Effect.mapError(destroyError)); | ||||||||||||||||
| const destroying = yield* output.task(`Destroying stack ${targetId}...`); | ||||||||||||||||
| const result = yield* stack.destroy.pipe( | ||||||||||||||||
| Effect.onExit((exit) => | ||||||||||||||||
| Exit.isSuccess(exit) | ||||||||||||||||
| ? destroying.clear | ||||||||||||||||
| : Cause.hasInterruptsOnly(exit.cause) | ||||||||||||||||
| ? destroying.cancel() | ||||||||||||||||
| : destroying.fail(Option.getOrUndefined(Exit.findErrorOption(exit))?.message), | ||||||||||||||||
| ), | ||||||||||||||||
| Effect.mapError(destroyError), | ||||||||||||||||
| ); | ||||||||||||||||
| return result; | ||||||||||||||||
| }); | ||||||||||||||||
|
|
||||||||||||||||
| const exit = yield* Effect.exit(destroySingle); | ||||||||||||||||
| if (Exit.isSuccess(exit)) { | ||||||||||||||||
| const result = exit.value; | ||||||||||||||||
| successes.push({ id: targetId, result }); | ||||||||||||||||
| if (result.runtimeCleanup === "skipped") | ||||||||||||||||
| yield* output.warn(skippedRuntimeCleanupWarning(`stack ${targetId}`, result)); | ||||||||||||||||
| if (output.format === "text") { | ||||||||||||||||
| if (result.runtimeCleanup === "complete") | ||||||||||||||||
| yield* output.raw(`Stack ${targetId} destroyed.\n`); | ||||||||||||||||
| else | ||||||||||||||||
| yield* output.raw( | ||||||||||||||||
| `Stack ${targetId} was removed locally; its ${result.engine === "docker" ? "Docker" : "Podman"} resources remain until the commands above are run.\n`, | ||||||||||||||||
| ); | ||||||||||||||||
| } | ||||||||||||||||
| } else { | ||||||||||||||||
| const error = Option.getOrElse( | ||||||||||||||||
| Exit.findErrorOption(exit), | ||||||||||||||||
| () => | ||||||||||||||||
| new StackCommandDestroyError({ | ||||||||||||||||
| reason: "unknown", | ||||||||||||||||
| message: `Failed to destroy stack ${targetId}`, | ||||||||||||||||
| }), | ||||||||||||||||
| ); | ||||||||||||||||
|
Comment on lines
+201
to
+208
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ig we should we keep the original error here? instead of a generic failure |
||||||||||||||||
| failures.push({ id: targetId, error }); | ||||||||||||||||
| yield* output.error(`Failed to destroy stack ${targetId}: ${error.message}`); | ||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. imo the task and final error already report this failure. so we prob can drop this extra error line on stdout as well.. |
||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| if (uniqueTargets.length === 1 && successes.length === 1) { | ||||||||||||||||
| yield* Effect.annotateCurrentSpan({ | ||||||||||||||||
| "stack.prompted": !yes, | ||||||||||||||||
| "stack.runtime_cleanup": successes[0]!.result.runtimeCleanup, | ||||||||||||||||
| }); | ||||||||||||||||
| } else { | ||||||||||||||||
| yield* Effect.annotateCurrentSpan({ | ||||||||||||||||
| "stack.prompted": !yes, | ||||||||||||||||
| "stack.count": uniqueTargets.length, | ||||||||||||||||
| "stack.destroyed_count": successes.length, | ||||||||||||||||
| "stack.failed_count": failures.length, | ||||||||||||||||
| }); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| if (output.format !== "text") { | ||||||||||||||||
| if (uniqueTargets.length === 1 && successes.length === 1) { | ||||||||||||||||
| yield* output.success("", { | ||||||||||||||||
|
tirthraj01 marked this conversation as resolved.
|
||||||||||||||||
| destroyed: true, | ||||||||||||||||
| id: successes[0]!.id, | ||||||||||||||||
| ...successes[0]!.result, | ||||||||||||||||
| }); | ||||||||||||||||
| } else { | ||||||||||||||||
| yield* output.success("", { | ||||||||||||||||
| destroyed: failures.length === 0, | ||||||||||||||||
| stacks: successes.map(({ id, result }) => ({ id, ...result })), | ||||||||||||||||
| failures: failures.map(({ id, error }) => ({ id, message: error.message })), | ||||||||||||||||
| }); | ||||||||||||||||
| } | ||||||||||||||||
|
Comment on lines
+236
to
+241
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could this emit one error envelope? failed destroys currently print two JSON documents cli/apps/cli/src/commands/experimental/stack/start/start.handler.ts Lines 722 to 728 in a0666ab
|
||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| if (failures.length > 0) { | ||||||||||||||||
| if (failures.length === 1 && uniqueTargets.length === 1) { | ||||||||||||||||
| return yield* failures[0]!.error; | ||||||||||||||||
| } | ||||||||||||||||
| return yield* new StackCommandDestroyError({ | ||||||||||||||||
| reason: "unknown", | ||||||||||||||||
| message: `Failed to destroy ${failures.length} managed stack(s).`, | ||||||||||||||||
| detail: failures.map(({ id, error }) => `${id}: ${error.message}`).join("\n"), | ||||||||||||||||
| cause: failures, | ||||||||||||||||
| }); | ||||||||||||||||
| } | ||||||||||||||||
| }); | ||||||||||||||||
| return yield* body.pipe(Effect.ensuring(telemetryState.flush)); | ||||||||||||||||
| }); | ||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could you also update
stack-commands.mdandSIDE_EFFECTS.md?