1import * as Effect from "effect/Effect";
2import type { Scope } from "effect/Scope";
3import { isResolved } from "../Diff.ts";
4import { createPhysicalName } from "../PhysicalName.ts";
5import {
6 Platform,
7 type PlatformProps,
8 type PlatformServices,
9} from "../Platform.ts";
10import * as Provider from "../Provider.ts";
11import { Resource } from "../Resource.ts";
12import {
13 packEnvValue,
14 unpackEnvValue,
15 RuntimeContext,
16} from "../RuntimeContext.ts";
17import type { HostRuntimeContext } from "../Server/Process.ts";
18import { Stack } from "../Stack.ts";
19import { createInternalTags } from "../Tags.ts";
20import * as Output from "../Output.ts";
21import { sha256Object } from "../Util/sha256.ts";
22import {
23 findClusterAdapter,
24 type IdentityState,
25 type RegistryState,
26 type WorkloadBindingContract,
27 type WorkloadIdentityOptions,
28 type WorkloadImageSource,
29 type WorkloadServices,
30} from "./ClusterAdapter.ts";
31import {
32 toConnection,
33 type ClusterLike,
34 type Connection,
35} from "./Connection.ts";
36import {
37 connectCluster,
38 deleteObjects,
39 readObject,
40 reconcileObjects,
41 KubernetesApiError,
42} from "./internal/client.ts";
43import type {
44 KubernetesObjectDefinition,
45 KubernetesObjectRef,
46} from "./internal/objects.ts";
47import {
48 collectBindingEnv,
49 connectionIdentity,
50 connectionOfOutput,
51 deepMerge,
52 imagePlatformOf,
53 makeJobBootstrap,
54 resolveWorkloadImage,
55 tryConnectionOf,
56 workloadImageHash,
57} from "./internal/workload.ts";
58import { makeConnectionRegistry } from "./internal/registry.ts";
59import type { Providers } from "./Providers.ts";
60
61export const isJob = (value: any): value is Job => {
62 return (
63 typeof value === "object" &&
64 value !== null &&
65 "Type" in value &&
66 value.Type === "Kubernetes.Job"
67 );
68};
69
70export interface JobPropsBase extends PlatformProps {
71 /**
72 * Target cluster the job runs on: a cluster resource
73 * (`Kubernetes.LocalCluster`, `AWS.EKS.Cluster`), a
74 * `Kubernetes.KubeConfig(...)`, or a raw `Kubernetes.Connection`. The
75 * connection supplies authentication and the registry `main` / `context`
76 * images are pushed to.
77 */
78 cluster: ClusterLike;
79 /**
80 * Base name for the generated Job / ServiceAccount. If omitted, a
81 * deterministic name is derived from the stack, stage, and logical id.
82 */
83 name?: string;
84 /**
85 * Kubernetes namespace to run in. The namespace must already exist.
86 * @default "default"
87 */
88 namespace?: string;
89 /**
90 * Number of retries before the Job is marked failed (Kubernetes
91 * `backoffLimit`).
92 */
93 backoffLimit?: number;
94 /**
95 * Cron schedule (standard 5-field cron, e.g. `"0 3 * * *"`). When set, a
96 * Kubernetes `CronJob` is synthesized instead of a plain `Job`.
97 */
98 schedule?: string;
99 /**
100 * Restart policy for the job's pods.
101 * @default "Never"
102 */
103 restartPolicy?: "Never" | "OnFailure";
104 /**
105 * Seconds after completion before the finished Job is garbage-collected
106 * (Kubernetes `ttlSecondsAfterFinished`).
107 */
108 ttlSecondsAfterFinished?: number;
109 /**
110 * Container entrypoint override (Kubernetes `command`). Mostly useful with
111 * `image` / `context` sources.
112 */
113 command?: string[];
114 /**
115 * Container arguments (Kubernetes `args`).
116 */
117 args?: string[];
118 /**
119 * Container CPU/memory requests + limits (Kubernetes resource quantities).
120 */
121 resources?: {
122 requests?: { cpu?: string; memory?: string };
123 limits?: { cpu?: string; memory?: string };
124 };
125 /**
126 * Additional environment variables for the container.
127 */
128 env?: Record<string, any>;
129 /**
130 * Container image build architecture.
131 * @default the connection's `architecture`, else "amd64"
132 */
133 architecture?: "amd64" | "arm64";
134 /**
135 * Deep-partial Kubernetes Pod template merged into the synthesized
136 * template (objects merge recursively; arrays and primitives replace) —
137 * a literal object in the shape of `PodTemplateSpec`, e.g.
138 * `{ spec: { tolerations: [...], nodeSelector: {...} } }`.
139 */
140 podTemplate?: Record<string, unknown>;
141 /**
142 * Cloud-specific workload-identity options, consumed by the cluster
143 * platform's identity adapter (on EKS: `{ managedPolicyArns: [...] }`
144 * attaches extra managed policies to the generated pod-identity role; on
145 * GKE: `{ gcpServiceAccount }` runs the pods as an existing Google
146 * service account instead of the ServiceAccount's own principal).
147 */
148 identity?: WorkloadIdentityOptions;
149 /**
150 * Job / pod labels, merged over the generated `app.kubernetes.io/name`
151 * label.
152 */
153 labels?: Record<string, string>;
154 /**
155 * User-defined tags applied to workload-owned cloud resources (identity
156 * roles, image repositories).
157 */
158 tags?: Record<string, string>;
159}
160
161/** Bundle an inline Effect program (`main`) into a generated image. */
162export interface BundledJobProps extends JobPropsBase {
163 /**
164 * Module entrypoint for the bundled program. This should typically be
165 * `import.meta.url` from an inline Effect program.
166 */
167 main: string;
168 /**
169 * Environment image used as the generated Dockerfile's `FROM`; must be
170 * able to run the bun runtime.
171 * @default "oven/bun:1"
172 */
173 image?: string;
174 /** Named export to load from `main`. @default "default" */
175 handler?: string;
176 /** Bundler configuration for the entrypoint. */
177 build?: WorkloadImageSource["build"];
178 /** Environment Dockerfile (path or inline content). */
179 dockerfile?: WorkloadImageSource["dockerfile"];
180 /** Build context for a path {@link dockerfile} environment. */
181 context?: string;
182}
183
184/** Build the user's own Dockerfile (`context` + optional `dockerfile` path). */
185export interface DockerfileJobProps extends JobPropsBase {
186 /** Docker build context directory. */
187 context?: string;
188 /**
189 * Path to the Dockerfile (relative to the cwd), or inline content.
190 * @default `${context}/Dockerfile`
191 */
192 dockerfile?: WorkloadImageSource["dockerfile"];
193}
194
195/** Run a pre-built registry image. */
196export interface ImageJobProps extends JobPropsBase {
197 /**
198 * A pre-built image reference, e.g. `ghcr.io/acme/migrator:v3`, pulled
199 * by the nodes as written. EKS clusters mirror it into ECR first.
200 */
201 image: string;
202}
203
204export type JobProps = BundledJobProps | DockerfileJobProps | ImageJobProps;
205
206export interface Job extends Resource<
207 "Kubernetes.Job",
208 JobProps,
209 {
210 /** The connection of the cluster the job runs on. */
211 connection: Connection;
212 /** The Kubernetes namespace the job's objects live in. */
213 namespace: string;
214 /** The Kubernetes kind synthesized for the workload (`Job`, or `CronJob` when `schedule` is set). */
215 kind: "Job" | "CronJob";
216 /** The name of the Kubernetes Job/CronJob object. */
217 jobName: string;
218 /** The cron schedule, when the workload is a CronJob. */
219 schedule: string | undefined;
220 /** The name of the service account the pods run as. */
221 serviceAccountName: string;
222 /** The URI of the container image the job runs. */
223 imageUri: string;
224 /**
225 * Workload-identity state provisioned by the cluster platform's
226 * adapter (on EKS: the pod-identity role + association; on GKE: the
227 * Workload Identity principal and the IAM grants applied to it).
228 */
229 identity: IdentityState | undefined;
230 /**
231 * Image-registry state provisioned by the cluster platform's adapter
232 * (on EKS: the ECR repository).
233 */
234 registry: RegistryState | undefined;
235 /** References to the Kubernetes objects created for the job. */
236 kubernetesObjects: KubernetesObjectRef[];
237 /** The content hash of the container image source. */
238 code: {
239 hash: string;
240 };
241 },
242 WorkloadBindingContract,
243 Providers
244> {}
245
246export type JobServices = WorkloadServices;
247
248/**
249 * The impl shape: `{ run, ...rpc }`. `run` executes to completion inside
250 * the pod; the process exits when it returns.
251 */
252export type JobMain<InitServices = never> = void | {
253 run?: Effect.Effect<
254 void,
255 never,
256 InitServices | PlatformServices | RuntimeContext | Scope
257 >;
258};
259
260export type JobShape = JobMain<JobServices>;
261
262export interface JobRuntimeContext extends HostRuntimeContext {
263 readonly Type: "Kubernetes.Job";
264}
265
266/**
267 * Run-to-completion work on any Kubernetes cluster, as a container image
268 * or an Effect program.
269 *
270 * `Job` provisions a Kubernetes `Job` (or `CronJob` when `schedule` is
271 * set) via server-side apply and a ServiceAccount, plus — through the
272 * target cluster's platform adapter — workload identity and a container
273 * image from exactly one of three sources flat on props: `main` (bundle an
274 * inline Effect program whose impl returns `{ run }`), `context` (build
275 * your own Dockerfile), or `image` (a pre-built registry reference).
276 * `main` and `context` images are built on the deploying machine and
277 * pushed to the connection's registry (`Kubernetes.LocalCluster` includes
278 * one; EKS uses ECR; GKE uses Artifact Registry). On `AWS.EKS.Cluster`
279 * targets, bindings attach IAM policy statements to a generated
280 * pod-identity role; on `GCP.Container.Cluster` targets, GCP bindings grant
281 * their IAM roles to the Kubernetes ServiceAccount's Workload Identity
282 * Federation principal — exactly like `Kubernetes.Deployment`.
283 * ### Creating a Job
284 * **Example:** Remote image (external — no Effect runtime in the container)
285 * ```typescript
286 * const migrate = yield* Kubernetes.Job("DbMigrate", {
287 * cluster,
288 * image: "ghcr.io/acme/migrator:v3",
289 * backoffLimit: 2,
290 * });
291 * ```
292 *
293 * **Example:** Inline Effect program
294 * ```typescript
295 * const cluster = yield* Kubernetes.LocalCluster("Cluster", {
296 * name: "alchemy",
297 * });
298 *
299 * const hello = yield* Kubernetes.Job(
300 * "Hello",
301 * { cluster, main: import.meta.url, backoffLimit: 2 },
302 * Effect.gen(function* () {
303 * return {
304 * run: Effect.log("hello from a Job"),
305 * };
306 * }),
307 * );
308 * ```
309 *
310 * **Example:** Inline Effect program with a DynamoDB binding (EKS)
311 * ```typescript
312 * const seed = yield* Kubernetes.Job(
313 * "SeedData",
314 * { cluster, main: import.meta.url },
315 * Effect.gen(function* () {
316 * const putItem = yield* AWS.DynamoDB.PutItem(table);
317 * return {
318 * run: Effect.gen(function* () {
319 * yield* putItem({ Item: { id: { S: "seed" } } });
320 * }),
321 * };
322 * }).pipe(Effect.provide(AWS.DynamoDB.PutItemHttp)),
323 * );
324 * ```
325 *
326 * **Example:** Tagged Effect program
327 * ```typescript
328 * export class Backfill extends Kubernetes.Job<Backfill, {
329 * progress: () => Effect.Effect<number>;
330 * }>()("Backfill") {}
331 *
332 * export default Backfill.make(
333 * { cluster, main: import.meta.url, backoffLimit: 1 },
334 * Effect.gen(function* () {
335 * return {
336 * run: Effect.gen(function* () { }),
337 * progress: () => Effect.succeed(0),
338 * };
339 * }),
340 * );
341 * ```
342 *
343 * ### Bundling & Tree-shaking
344 * `main` is bundled with rolldown at deploy time. Unused code is
345 * tree-shaken. `effect`, alchemy, and `@distilled.cloud` are marked
346 * pure so unused parts prune more aggressively. Your app is not
347 * marked pure.
348 *
349 * **Example:** Mark additional packages as pure
350 * Only list packages with no top-level side effects.
351 * ```typescript
352 * {
353 * main: import.meta.url,
354 * build: {
355 * pure: { packages: ["my-lib", "@my-scope/*"] },
356 * },
357 * }
358 * ```
359 *
360 * **Example:** Turn it off
361 * ```typescript
362 * {
363 * main: import.meta.url,
364 * build: { pure: false },
365 * }
366 * ```
367 *
368 * ### Scheduling
369 * **Example:** Nightly CronJob
370 * ```typescript
371 * const nightly = yield* Kubernetes.Job("NightlyBackfill", {
372 * cluster,
373 * main: import.meta.url,
374 * schedule: "0 3 * * *",
375 * });
376 * ```
377 *
378 * @resource
379 * @product Workloads
380 */
381export const Job: Platform<Job, JobServices, JobShape, JobRuntimeContext> =
382 Platform("Kubernetes.Job", {
383 aliases: ["AWS.EKS.Job"],
384 createRuntimeContext: (id: string): JobRuntimeContext => {
385 // A one-shot host context: `serve` (invoked by the Platform machinery
386 // with the impl shape) registers the shape's `run` effect as the
387 // program instead of an HTTP server, so the generated entry executes
388 // it to completion and exits.
389 const runners: Effect.Effect<void, never, any>[] = [];
390 const env: Record<string, any> = {};
391 const context: JobRuntimeContext = {
392 Type: "Kubernetes.Job",
393 id,
394 env,
395 set: (bindingId: string, output: Output.Output) =>
396 Effect.sync(() => {
397 const key = bindingId.replaceAll(/[^a-zA-Z0-9]/g, "_");
398 env[key] = output.pipe(Output.map(packEnvValue));
399 return key;
400 }),
401 get: <T>(key: string) =>
402 Effect.sync(() => unpackEnvValue<T>(process.env[key]) as T),
403 run: (effect: Effect.Effect<void, never, any>) =>
404 Effect.sync(() => {
405 runners.push(effect);
406 }),
407 serve: ((_handler, options) =>
408 Effect.sync(() => {
409 const run = options?.shape?.run;
410 if (Effect.isEffect(run)) {
411 runners.push(run as Effect.Effect<void, never, any>);
412 }
413 })) as HostRuntimeContext["serve"],
414 exports: Effect.sync(() => ({
415 program: Effect.all(runners, { concurrency: "unbounded" }).pipe(
416 Effect.asVoid,
417 ),
418 })),
419 };
420 return context;
421 },
422 });
423
424const isNotFound = (error: unknown): error is KubernetesApiError =>
425 error instanceof KubernetesApiError && error.statusCode === 404;
426
427export const JobProvider = () =>
428 Provider.effect(
429 Job,
430 Effect.gen(function* () {
431 const stack = yield* Stack;
432 const connectionRegistry = yield* makeConnectionRegistry;
433
434 const alchemyEnv = {
435 ALCHEMY_STACK_NAME: stack.name,
436 ALCHEMY_STAGE: stack.stage,
437 ALCHEMY_PHASE: "runtime",
438 };
439
440 // The base name doubles as the `app.kubernetes.io/name` label value
441 // (and the Service name), both capped at 63 characters.
442 const toBaseName = (id: string, props: { name?: string } = {}) =>
443 props.name
444 ? Effect.succeed(props.name)
445 : createPhysicalName({ id, maxLength: 63, lowercase: true }).pipe(
446 Effect.map((name) => name.replaceAll(/[^a-z0-9-]/g, "-")),
447 );
448
449 return {
450 stables: [
451 "connection",
452 "namespace",
453 "serviceAccountName",
454 "identity",
455 "registry",
456 ],
457 // A Job's identity spans in-cluster Kubernetes objects plus
458 // adapter-owned cloud resources — no single enumeration
459 // reconstructs the composite, so enumeration is empty; `read`
460 // refreshes known instances.
461 list: () => Effect.succeed([] as Job["Attributes"][]),
462 diff: Effect.fn(function* ({
463 olds = {} as JobProps,
464 news: input,
465 output,
466 }) {
467 // `exports` carries the program's runtime Effects (never plain
468 // data); everything else must be resolved to diff.
469 const { exports: _exports, ...declared } = input as typeof input & {
470 exports?: unknown;
471 };
472 if (!isResolved(declared)) return;
473 const news = input as unknown as JobProps;
474 const oldCluster = connectionIdentity(tryConnectionOf(olds.cluster));
475 const newCluster = connectionIdentity(tryConnectionOf(news.cluster));
476 if (
477 oldCluster !== undefined &&
478 newCluster !== undefined &&
479 oldCluster !== newCluster
480 ) {
481 return { action: "replace" } as const;
482 }
483 if (
484 oldCluster !== undefined &&
485 (olds.namespace ?? "default") !== (news.namespace ?? "default")
486 ) {
487 return { action: "replace" } as const;
488 }
489 // Content drift (see Deployment).
490 const connection = tryConnectionOf(news.cluster);
491 if (output && connection) {
492 const adapter = yield* findClusterAdapter(connection.auth.kind);
493 const source = news as WorkloadImageSource;
494 const hash = yield* workloadImageHash({
495 adapter,
496 connection,
497 connectionRegistry,
498 source,
499 platform: imagePlatformOf(news.architecture, connection),
500 isExternal: news.isExternal,
501 bootstrap: (adapter.bootstrap?.job ?? makeJobBootstrap)(
502 source.handler ?? "default",
503 ),
504 });
505 if (hash !== undefined && hash !== output.code.hash) {
506 return { action: "update" } as const;
507 }
508 }
509 }),
510 read: Effect.fn(function* ({ output }) {
511 if (!output) return undefined;
512 const connection = connectionOfOutput(output);
513 if (!connection) return undefined;
514 const transport = yield* connectCluster(connection).pipe(
515 Effect.catchTag("Kubernetes.ClusterNotFoundError", () =>
516 Effect.succeed(undefined),
517 ),
518 // Transient unreachability must not read as "gone".
519 Effect.catch(() => Effect.succeed("unreachable" as const)),
520 );
521 if (transport === undefined) return undefined;
522 if (transport === "unreachable") return output;
523 // The ServiceAccount is the stable existence anchor: one-shot Job
524 // objects are content-addressed and may have been TTL-collected.
525 const anchor = (output.kubernetesObjects ?? [])[0];
526 if (!anchor) return output;
527 const observed = yield* readObject({
528 transport,
529 object: anchor,
530 }).pipe(
531 Effect.catchIf(isNotFound, () => Effect.succeed(undefined)),
532 Effect.catch(() => Effect.succeed(output)),
533 );
534 if (observed === undefined) return undefined;
535 return output;
536 }),
537 reconcile: Effect.fn(function* ({
538 id,
539 news,
540 bindings,
541 output,
542 session,
543 }) {
544 const connection = toConnection(news.cluster);
545 const adapter = yield* findClusterAdapter(connection.auth.kind);
546 const transport = yield* adapter.connect(connection);
547 const namespace = news.namespace ?? "default";
548
549 const baseName = yield* toBaseName(id, news);
550 const serviceAccountName = output?.serviceAccountName ?? baseName;
551 const tags = {
552 ...(yield* createInternalTags(id)),
553 ...news.tags,
554 };
555
556 const { env: bindingEnv, grantKeys } = collectBindingEnv(bindings);
557 if (grantKeys.length > 0 && adapter.identity === undefined) {
558 return yield* Effect.die(
559 new Error(
560 `'${id}': bindings carry cloud credential grants ` +
561 `(${grantKeys.join(", ")}) but this cluster's platform ` +
562 "has no workload-identity adapter. Target a managed " +
563 "cluster (e.g. AWS.EKS.Cluster) or bind through " +
564 "environment variables instead.",
565 ),
566 );
567 }
568 const identity = adapter.identity
569 ? yield* adapter.identity.reconcile({
570 id,
571 connection,
572 namespace,
573 serviceAccount: serviceAccountName,
574 bindings,
575 options: news.identity,
576 state:
577 (output?.identity as Record<string, unknown> | undefined) ??
578 (output as Record<string, unknown> | undefined),
579 tags,
580 })
581 : undefined;
582
583 // Resolve the container image (`main` | `context` | `image`).
584 const source = news as WorkloadImageSource;
585 const resolved = yield* resolveWorkloadImage({
586 adapter,
587 connection,
588 connectionRegistry,
589 id,
590 source,
591 platform: imagePlatformOf(news.architecture, connection),
592 isExternal: news.isExternal,
593 bootstrap: (adapter.bootstrap?.job ?? makeJobBootstrap)(
594 source.handler ?? "default",
595 ),
596 tags,
597 state:
598 (output?.registry as Record<string, unknown> | undefined) ??
599 (output as Record<string, unknown> | undefined),
600 session,
601 });
602
603 // Synthesize the Kubernetes objects. The generated name label is
604 // always present so selectors and Kueue-style extra labels
605 // compose without erasing the workload's identity label.
606 const labels = {
607 "app.kubernetes.io/name": baseName,
608 ...news.labels,
609 };
610 const containerEnv = {
611 ...bindingEnv,
612 ...identity?.env,
613 ...alchemyEnv,
614 ...news.env,
615 };
616
617 const serviceAccountObject: KubernetesObjectDefinition = {
618 apiVersion: "v1",
619 kind: "ServiceAccount",
620 metadata: {
621 name: serviceAccountName,
622 namespace,
623 labels,
624 ...(identity?.serviceAccountAnnotations
625 ? { annotations: identity.serviceAccountAnnotations }
626 : {}),
627 },
628 };
629 const podTemplate = deepMerge(
630 {
631 metadata: { labels },
632 spec: {
633 serviceAccountName,
634 restartPolicy: news.restartPolicy ?? "Never",
635 containers: [
636 {
637 name: baseName,
638 image: resolved.imageUri,
639 command: news.command,
640 args: news.args,
641 env: Object.entries(containerEnv).map(([name, value]) => ({
642 name,
643 value:
644 typeof value === "string"
645 ? value
646 : JSON.stringify(value),
647 })),
648 resources: news.resources,
649 },
650 ],
651 },
652 },
653 news.podTemplate,
654 );
655 const jobSpec = {
656 backoffLimit: news.backoffLimit,
657 ttlSecondsAfterFinished: news.ttlSecondsAfterFinished,
658 template: podTemplate,
659 };
660
661 const kind: "Job" | "CronJob" = news.schedule ? "CronJob" : "Job";
662
663 // A Kubernetes Job's pod template is immutable after creation, so
664 // one-shot Jobs are content-addressed: the object name embeds a
665 // hash of the spec, and a spec change applies a NEW Job (which
666 // runs) while `reconcileObjects` deletes the previous one.
667 // CronJobs are mutable and keep the stable base name.
668 // Kubernetes rejects Job names over 63 characters (the API
669 // stamps the name into the batch.kubernetes.io/job-name pod
670 // label, and label values cap at 63) — truncate the base so the
671 // content-address suffix always fits.
672 const jobName = news.schedule
673 ? baseName.slice(0, 52).replace(/-+$/, "")
674 : `${baseName.slice(0, 54).replace(/-+$/, "")}-${(yield* sha256Object(jobSpec)).slice(0, 8)}`;
675
676 const workloadObject: KubernetesObjectDefinition = news.schedule
677 ? {
678 apiVersion: "batch/v1",
679 kind: "CronJob",
680 metadata: { name: jobName, namespace, labels },
681 spec: {
682 schedule: news.schedule,
683 jobTemplate: {
684 metadata: { labels },
685 spec: jobSpec,
686 },
687 },
688 }
689 : {
690 apiVersion: "batch/v1",
691 kind: "Job",
692 metadata: { name: jobName, namespace, labels },
693 spec: jobSpec,
694 };
695
696 const desiredObjects = [serviceAccountObject, workloadObject];
697
698 const kubernetesObjects = yield* reconcileObjects({
699 transport,
700 previousObjects: output?.kubernetesObjects ?? [],
701 desiredObjects,
702 });
703
704 yield* session.note(
705 `Applied Kubernetes ${kind} ${namespace}/${jobName}`,
706 );
707
708 return {
709 connection,
710 namespace,
711 kind,
712 jobName,
713 schedule: news.schedule,
714 serviceAccountName,
715 imageUri: resolved.imageUri,
716 identity: identity?.state,
717 registry: resolved.state,
718 kubernetesObjects,
719 code: { hash: resolved.codeHash },
720 };
721 }),
722 delete: Effect.fn(function* ({ output }) {
723 const connection = connectionOfOutput(output);
724 if (!connection) return;
725 const adapter = yield* findClusterAdapter(connection.auth.kind);
726
727 // Delete the in-cluster objects; skip when the cluster is gone
728 // (cluster-scoped state dies with it) and still clean up the
729 // adapter-owned cloud resources that outlive it.
730 const transport = yield* adapter
731 .connect(connection)
732 .pipe(Effect.catch(() => Effect.succeed(undefined)));
733 if (transport && (output.kubernetesObjects ?? []).length > 0) {
734 yield* deleteObjects({
735 transport,
736 objects: output.kubernetesObjects ?? [],
737 }).pipe(Effect.catch(() => Effect.void));
738 }
739
740 if (adapter.identity) {
741 yield* adapter.identity.delete({
742 connection,
743 state:
744 (output.identity as Record<string, unknown> | undefined) ??
745 (output as Record<string, unknown>),
746 });
747 }
748 if (adapter.registry) {
749 yield* adapter.registry.delete({
750 state:
751 (output.registry as Record<string, unknown> | undefined) ??
752 (output as Record<string, unknown>),
753 });
754 }
755 }),
756 };
757 }),
758 );