| 1 | import * as Config from "effect/Config"; |
| 2 | import * as Context from "effect/Context"; |
| 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as Layer from "effect/Layer"; |
| 5 | import * as Logger from "effect/Logger"; |
| 6 | import * as Option from "effect/Option"; |
| 7 | import { MinimumLogLevel } from "effect/References"; |
| 8 | import * as Nuke from "../../Nuke.ts"; |
| 9 | import type { ProviderMode } from "../../ProviderMode.ts"; |
| 10 | import type { ApplyStatus } from "../../Report.ts"; |
| 11 | import { Progress, withSpanEvents } from "../Progress.ts"; |
| 12 | import { |
| 13 | buildStackProviders, |
| 14 | DEFAULT_ENTRYPOINT, |
| 15 | type Target, |
| 16 | } from "../Session.ts"; |
| 17 | |
| 18 | export 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. */ |
| 29 | export type NukeResource = Nuke.Target; |
| 30 | |
| 31 | export 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 | |
| 39 | export 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 | |
| 48 | export type NukeResult = Nuke.Result; |
| 49 | |
| 50 | /** Enumerate everything the stack's registered providers can see. */ |
| 51 | export 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 | */ |
| 94 | export 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 | }); |