1import * as Config from "effect/Config";
2import * as Context from "effect/Context";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Logger from "effect/Logger";
6import * as Option from "effect/Option";
7import { MinimumLogLevel } from "effect/References";
8import * as Nuke from "../../Nuke.ts";
9import type { ProviderMode } from "../../ProviderMode.ts";
10import type { ApplyStatus } from "../../Report.ts";
11import { Progress, withSpanEvents } from "../Progress.ts";
12import {
13 buildStackProviders,
14 DEFAULT_ENTRYPOINT,
15 type Target,
16} from "../Session.ts";
17
18export interface ScanInput extends Target {
19 readonly mode: ProviderMode;
20 /** Provider-id globs to include. Omitted means every provider. */
21 readonly include?: ReadonlyArray<string>;
22 /** Provider-id globs to exclude. Applied after `include`. */
23 readonly exclude?: ReadonlyArray<string>;
24 readonly concurrency?: number | "unbounded";
25 readonly providerTimeoutSeconds?: number;
26}
27
28/** One discovered cloud object, bound to the provider that can delete it. */
29export type NukeResource = Nuke.Target;
30
31export interface NukeScan {
32 readonly mode: ProviderMode;
33 readonly resources: ReadonlyArray<NukeResource>;
34 readonly failures: ReadonlyArray<Nuke.ProviderFailure>;
35 /** The built provider context {@link execute} deletes under. */
36 readonly context: Context.Context<never>;
37}
38
39export interface ExecuteInput {
40 readonly scan: NukeScan;
41 /** The subset of `scan.resources` to delete. */
42 readonly resources: ReadonlyArray<NukeResource>;
43 readonly strategy: Nuke.Strategy;
44 readonly concurrency?: number | "unbounded";
45 readonly providerTimeoutSeconds?: number;
46}
47
48export type NukeResult = Nuke.Result;
49
50/** Enumerate everything the stack's registered providers can see. */
51export const scan = Effect.fn("Alchemist.nuke.scan")(function* (
52 input: ScanInput,
53) {
54 const report = withSpanEvents(yield* Progress);
55 const debug = yield* Config.String("DEBUG").pipe(
56 Config.withDefault(""),
57 Effect.map((value) => value.length > 0),
58 );
59 // Nuke needs the provider layer, not a stack instance: it deletes what
60 // the cloud has, not what state says we own.
61 const built = yield* buildStackProviders({
62 main: input.entrypoint ?? DEFAULT_ENTRYPOINT,
63 envFile: Option.fromNullishOr(input.envFile),
64 profile: input.profile,
65 logger: debug ? Logger.layer([Logger.defaultLogger]) : undefined,
66 extra: Layer.succeed(MinimumLogLevel, debug ? "Debug" : "Info"),
67 });
68 const context = built.context as Context.Context<never>;
69 const { resources, failures } = yield* Nuke.list({
70 context,
71 mode: input.mode,
72 include: input.include,
73 exclude: input.exclude,
74 concurrency: input.concurrency,
75 timeoutSeconds: input.providerTimeoutSeconds,
76 onScan: (total) => report({ _tag: "nuke.scan.started", total }),
77 onProviderStarted: (provider) =>
78 report({ _tag: "nuke.scan.provider.started", provider }),
79 onProvider: (provider, count, error) =>
80 report({
81 _tag: "nuke.scan.provider.completed",
82 provider,
83 resources: count,
84 error,
85 }),
86 });
87 return { mode: input.mode, resources, failures, context } satisfies NukeScan;
88});
89
90/**
91 * Permanently delete the selected resources. Each confirmed deletion is
92 * reported through {@link Progress} as `NukeResourceDeleted`.
93 */
94export const execute = Effect.fn("Alchemist.nuke.execute")(function* (
95 input: ExecuteInput,
96) {
97 const report = withSpanEvents(yield* Progress);
98 const keys = new Map(
99 input.resources.map((resource, index) => [resource, `nuke/${index}`]),
100 );
101 const status = (
102 resource: Nuke.Target,
103 status: ApplyStatus,
104 message?: string,
105 ) =>
106 report({
107 _tag: "apply.resource.status",
108 fqn: keys.get(resource)!,
109 id: resource.providerId,
110 type: resource.providerId,
111 status,
112 message,
113 });
114 return yield* Nuke.destroy({
115 targets: input.resources,
116 context: input.scan.context,
117 strategy: input.strategy,
118 concurrency: input.concurrency,
119 timeoutSeconds: input.providerTimeoutSeconds,
120 onPass: (pass) => report({ _tag: "nuke.pass.started", pass }),
121 onDeleting: (resource) => status(resource, "deleting"),
122 onHeld: (resource, blockedBy) =>
123 status(resource, "skipped", `Held back by ${blockedBy.join(", ")}`),
124 onDeleted: (resource) =>
125 status(resource, "deleted").pipe(
126 Effect.andThen(
127 report({
128 _tag: "nuke.resource.deleted",
129 provider: resource.providerId,
130 resource: resource.displayName,
131 }),
132 ),
133 ),
134 onFailed: (resource, message) =>
135 status(resource, "fail", message).pipe(
136 Effect.andThen(
137 report({
138 _tag: "nuke.resource.failed",
139 provider: resource.providerId,
140 resource: resource.displayName,
141 message,
142 }),
143 ),
144 ),
145 });
146});