pulumi-sandbox 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +282 -0
  3. package/dist/actions.d.ts +7 -0
  4. package/dist/actions.d.ts.map +1 -0
  5. package/dist/actions.js +14 -0
  6. package/dist/backend.d.ts +40 -0
  7. package/dist/backend.d.ts.map +1 -0
  8. package/dist/backend.js +40 -0
  9. package/dist/docker/exec.d.ts +22 -0
  10. package/dist/docker/exec.d.ts.map +1 -0
  11. package/dist/docker/exec.js +26 -0
  12. package/dist/docker/index.d.ts +12 -0
  13. package/dist/docker/index.d.ts.map +1 -0
  14. package/dist/docker/index.js +8 -0
  15. package/dist/docker/mask-volumes.d.ts +74 -0
  16. package/dist/docker/mask-volumes.d.ts.map +1 -0
  17. package/dist/docker/mask-volumes.js +97 -0
  18. package/dist/docker/shell.d.ts +23 -0
  19. package/dist/docker/shell.d.ts.map +1 -0
  20. package/dist/docker/shell.js +34 -0
  21. package/dist/environment-file.d.ts +58 -0
  22. package/dist/environment-file.d.ts.map +1 -0
  23. package/dist/environment-file.js +129 -0
  24. package/dist/errors.d.ts +43 -0
  25. package/dist/errors.d.ts.map +1 -0
  26. package/dist/errors.js +47 -0
  27. package/dist/git.d.ts +11 -0
  28. package/dist/git.d.ts.map +1 -0
  29. package/dist/git.js +24 -0
  30. package/dist/identity.d.ts +55 -0
  31. package/dist/identity.d.ts.map +1 -0
  32. package/dist/identity.js +94 -0
  33. package/dist/index.d.ts +28 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +19 -0
  36. package/dist/interactive.d.ts +15 -0
  37. package/dist/interactive.d.ts.map +1 -0
  38. package/dist/interactive.js +45 -0
  39. package/dist/lifecycle.d.ts +41 -0
  40. package/dist/lifecycle.d.ts.map +1 -0
  41. package/dist/lifecycle.js +110 -0
  42. package/dist/outputs.d.ts +30 -0
  43. package/dist/outputs.d.ts.map +1 -0
  44. package/dist/outputs.js +65 -0
  45. package/dist/readiness.d.ts +57 -0
  46. package/dist/readiness.d.ts.map +1 -0
  47. package/dist/readiness.js +66 -0
  48. package/dist/sandbox.d.ts +172 -0
  49. package/dist/sandbox.d.ts.map +1 -0
  50. package/dist/sandbox.js +260 -0
  51. package/dist/state-surgery.d.ts +34 -0
  52. package/dist/state-surgery.d.ts.map +1 -0
  53. package/dist/state-surgery.js +94 -0
  54. package/dist/terminal.d.ts +35 -0
  55. package/dist/terminal.d.ts.map +1 -0
  56. package/dist/terminal.js +67 -0
  57. package/package.json +59 -0
@@ -0,0 +1,28 @@
1
+ /**
2
+ * pulumi-sandbox — local development sandboxes as code.
3
+ *
4
+ * Write a plain Pulumi inline program describing the infrastructure your
5
+ * application needs on a developer machine; this library supplies everything
6
+ * around it: a self-contained local backend, per-developer stack isolation,
7
+ * a complete lifecycle command line, recovery from half-dead environments,
8
+ * and the helpers that turn resolved outputs into environment files.
9
+ */
10
+ export { sandbox, createSandbox, Sandbox, DEFAULT_PASSPHRASE } from "./sandbox.js";
11
+ export type { SandboxOptions, SandboxProgram, SandboxOutputs, SandboxContext, SandboxCommand, SandboxCommandContext, } from "./sandbox.js";
12
+ export { LIFECYCLE_ACTIONS, ACTION_DESCRIPTIONS, isLifecycleAction } from "./actions.js";
13
+ export type { LifecycleAction } from "./actions.js";
14
+ export { fileBackendUrl, resolveDirectories, ensureDirectories } from "./backend.js";
15
+ export type { SandboxDirectories } from "./backend.js";
16
+ export { resolveDevId, parseEnvFile, readEnvFile, booleanFlag, DEV_ID_VARIABLE, DEFAULT_DEV_ID, } from "./identity.js";
17
+ export type { DevIdOptions } from "./identity.js";
18
+ export { EnvironmentFile } from "./environment-file.js";
19
+ export type { EnvironmentValue, EnvironmentValues } from "./environment-file.js";
20
+ export { deepResolve } from "./outputs.js";
21
+ export type { DeepResolved } from "./outputs.js";
22
+ export { waitForHttp, readyWhenHttp } from "./readiness.js";
23
+ export type { HttpProbeOptions, ReadyWhenHttpOptions } from "./readiness.js";
24
+ export { findGitRoot } from "./git.js";
25
+ export { purgeProviderFromState, removeProviderFromDeployment } from "./state-surgery.js";
26
+ export type { ProviderPurge } from "./state-surgery.js";
27
+ export { SandboxError, SandboxConfigurationError, SandboxLockError, EnvironmentFileError } from "./errors.js";
28
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACnF,YAAY,EACV,cAAc,EACd,cAAc,EACd,cAAc,EACd,cAAc,EACd,cAAc,EACd,qBAAqB,GACtB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACzF,YAAY,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpD,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACrF,YAAY,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,WAAW,EACX,eAAe,EACf,cAAc,GACf,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAEjF,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,YAAY,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC5D,YAAY,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAE7E,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAEvC,OAAO,EAAE,sBAAsB,EAAE,4BAA4B,EAAE,MAAM,oBAAoB,CAAC;AAC1F,YAAY,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD,OAAO,EAAE,YAAY,EAAE,yBAAyB,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * pulumi-sandbox — local development sandboxes as code.
3
+ *
4
+ * Write a plain Pulumi inline program describing the infrastructure your
5
+ * application needs on a developer machine; this library supplies everything
6
+ * around it: a self-contained local backend, per-developer stack isolation,
7
+ * a complete lifecycle command line, recovery from half-dead environments,
8
+ * and the helpers that turn resolved outputs into environment files.
9
+ */
10
+ export { sandbox, createSandbox, Sandbox, DEFAULT_PASSPHRASE } from "./sandbox.js";
11
+ export { LIFECYCLE_ACTIONS, ACTION_DESCRIPTIONS, isLifecycleAction } from "./actions.js";
12
+ export { fileBackendUrl, resolveDirectories, ensureDirectories } from "./backend.js";
13
+ export { resolveDevId, parseEnvFile, readEnvFile, booleanFlag, DEV_ID_VARIABLE, DEFAULT_DEV_ID, } from "./identity.js";
14
+ export { EnvironmentFile } from "./environment-file.js";
15
+ export { deepResolve } from "./outputs.js";
16
+ export { waitForHttp, readyWhenHttp } from "./readiness.js";
17
+ export { findGitRoot } from "./git.js";
18
+ export { purgeProviderFromState, removeProviderFromDeployment } from "./state-surgery.js";
19
+ export { SandboxError, SandboxConfigurationError, SandboxLockError, EnvironmentFileError } from "./errors.js";
@@ -0,0 +1,15 @@
1
+ import { type LifecycleAction } from "./actions.js";
2
+ /** What the interactive menu needs from the sandbox driving it. */
3
+ export interface InteractiveHost {
4
+ run(action: LifecycleAction): Promise<void>;
5
+ /** Renders a failed action without ending the session. */
6
+ reportFailure(error: unknown): void;
7
+ }
8
+ /**
9
+ * A small menu over the lifecycle actions, used when the sandbox is invoked
10
+ * without an action. Failed actions are reported and the menu returns, so a
11
+ * developer can run `cancel` right after a lock error or retry a `create`
12
+ * without restarting the process.
13
+ */
14
+ export declare function runInteractiveMenu(host: InteractiveHost): Promise<void>;
15
+ //# sourceMappingURL=interactive.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interactive.d.ts","sourceRoot":"","sources":["../src/interactive.ts"],"names":[],"mappings":"AACA,OAAO,EAA6D,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAG/G,mEAAmE;AACnE,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,0DAA0D;IAC1D,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;CACrC;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAoC7E"}
@@ -0,0 +1,45 @@
1
+ import readline from "node:readline/promises";
2
+ import { ACTION_DESCRIPTIONS, LIFECYCLE_ACTIONS, isLifecycleAction } from "./actions.js";
3
+ import { bold, cyan, dim, warn } from "./terminal.js";
4
+ /**
5
+ * A small menu over the lifecycle actions, used when the sandbox is invoked
6
+ * without an action. Failed actions are reported and the menu returns, so a
7
+ * developer can run `cancel` right after a lock error or retry a `create`
8
+ * without restarting the process.
9
+ */
10
+ export async function runInteractiveMenu(host) {
11
+ const input = readline.createInterface({ input: process.stdin, output: process.stdout });
12
+ // End-of-input (Ctrl+D, a closed pipe) must end the menu, not leave the
13
+ // pending question unsettled forever.
14
+ const closed = new Promise((resolve) => {
15
+ input.once("close", () => resolve("exit"));
16
+ });
17
+ try {
18
+ while (true) {
19
+ process.stdout.write("\n");
20
+ LIFECYCLE_ACTIONS.forEach((action, index) => {
21
+ process.stdout.write(` ${cyan(String(index + 1))} ${bold(action.padEnd(8))} ${dim(ACTION_DESCRIPTIONS[action])}\n`);
22
+ });
23
+ process.stdout.write(` ${cyan("0")} ${bold("exit".padEnd(8))} ${dim("Leave the menu")}\n\n`);
24
+ const answer = (await Promise.race([input.question(`${cyan("›")} action: `), closed])).trim().toLowerCase();
25
+ if (answer === "0" || answer === "exit" || answer === "quit" || answer === "q") {
26
+ return;
27
+ }
28
+ const byNumber = /^[0-9]+$/.test(answer) ? LIFECYCLE_ACTIONS[Number(answer) - 1] : undefined;
29
+ const action = byNumber ?? (isLifecycleAction(answer) ? answer : undefined);
30
+ if (action === undefined) {
31
+ warn(`"${answer}" is not an action — pick a number or name from the list.`);
32
+ continue;
33
+ }
34
+ try {
35
+ await host.run(action);
36
+ }
37
+ catch (error) {
38
+ host.reportFailure(error);
39
+ }
40
+ }
41
+ }
42
+ finally {
43
+ input.close();
44
+ }
45
+ }
@@ -0,0 +1,41 @@
1
+ import { automation } from "@pulumi/pulumi";
2
+ /** Everything the lifecycle operations need to act on a stack. */
3
+ export interface StackHost {
4
+ stack: automation.Stack;
5
+ stackName: string;
6
+ /**
7
+ * Names of provider resources whose backing service runs in a container
8
+ * this sandbox manages — see {@link purgeProviderFromState} for why these
9
+ * need special treatment.
10
+ */
11
+ containerHostedProviders: readonly string[];
12
+ }
13
+ /**
14
+ * Creates or updates the stack with a single `up --refresh`: one operation
15
+ * reconciles state with reality (containers killed by hand drop out) and
16
+ * applies the program.
17
+ *
18
+ * There is deliberately no separate pre-refresh: refresh initializes every
19
+ * provider in state, so a container-hosted provider whose container is gone
20
+ * would abort the run before the update gets a chance to recreate it. If the
21
+ * combined operation still fails, the container-hosted providers are purged
22
+ * from state and the update retried once — after the purge, refresh only
23
+ * sees providers it can reach, and the program re-registers the purged
24
+ * resources against the freshly created container.
25
+ */
26
+ export declare function createStack(host: StackHost): Promise<void>;
27
+ /**
28
+ * Destroys the stack. Container-hosted providers are purged from state
29
+ * first, so the destroy plan never asks an unreachable service to delete
30
+ * resources its own container teardown wipes physically; `refresh: true`
31
+ * then reconciles whatever survives the purge against reality before
32
+ * deleting it.
33
+ */
34
+ export declare function destroyStack(host: StackHost): Promise<void>;
35
+ /** Previews what {@link createStack} would change, with a full diff. */
36
+ export declare function previewStack(host: StackHost): Promise<void>;
37
+ /** Releases a stuck state lock left behind by an interrupted operation. */
38
+ export declare function cancelStack(host: StackHost): Promise<void>;
39
+ /** Prints the stack outputs as a JSON object. */
40
+ export declare function printOutputs(host: StackHost): Promise<void>;
41
+ //# sourceMappingURL=lifecycle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../src/lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAK5C,kEAAkE;AAClE,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,UAAU,CAAC,KAAK,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,wBAAwB,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7C;AASD;;;;;;;;;;;;GAYG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAuBhE;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAcjE;AAED,wEAAwE;AACxE,wBAAsB,YAAY,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAQjE;AAED,2EAA2E;AAC3E,wBAAsB,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAGhE;AAED,iDAAiD;AACjD,wBAAsB,YAAY,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAIjE"}
@@ -0,0 +1,110 @@
1
+ import { automation } from "@pulumi/pulumi";
2
+ import { SandboxLockError } from "./errors.js";
3
+ import { purgeProviderFromState } from "./state-surgery.js";
4
+ import { colorMode, detail, step, warn } from "./terminal.js";
5
+ function streamOptions() {
6
+ return {
7
+ onOutput: (data) => process.stdout.write(data),
8
+ color: colorMode(),
9
+ };
10
+ }
11
+ /**
12
+ * Creates or updates the stack with a single `up --refresh`: one operation
13
+ * reconciles state with reality (containers killed by hand drop out) and
14
+ * applies the program.
15
+ *
16
+ * There is deliberately no separate pre-refresh: refresh initializes every
17
+ * provider in state, so a container-hosted provider whose container is gone
18
+ * would abort the run before the update gets a chance to recreate it. If the
19
+ * combined operation still fails, the container-hosted providers are purged
20
+ * from state and the update retried once — after the purge, refresh only
21
+ * sees providers it can reach, and the program re-registers the purged
22
+ * resources against the freshly created container.
23
+ */
24
+ export async function createStack(host) {
25
+ step("Refreshing and updating the stack");
26
+ try {
27
+ await host.stack.up({ ...streamOptions(), refresh: true });
28
+ }
29
+ catch (error) {
30
+ rethrowLockError(error, host.stackName);
31
+ if (host.containerHostedProviders.length === 0) {
32
+ throw error;
33
+ }
34
+ warn("The update failed; assuming a container-hosted service is gone, purging its state, and retrying once.");
35
+ try {
36
+ await purgeContainerHostedProviders(host);
37
+ }
38
+ catch (purgeError) {
39
+ rethrowLockError(purgeError, host.stackName);
40
+ throw purgeError;
41
+ }
42
+ try {
43
+ await host.stack.up({ ...streamOptions(), refresh: true });
44
+ }
45
+ catch (retryError) {
46
+ rethrowLockError(retryError, host.stackName);
47
+ throw retryError;
48
+ }
49
+ }
50
+ }
51
+ /**
52
+ * Destroys the stack. Container-hosted providers are purged from state
53
+ * first, so the destroy plan never asks an unreachable service to delete
54
+ * resources its own container teardown wipes physically; `refresh: true`
55
+ * then reconciles whatever survives the purge against reality before
56
+ * deleting it.
57
+ */
58
+ export async function destroyStack(host) {
59
+ try {
60
+ await purgeContainerHostedProviders(host);
61
+ }
62
+ catch (error) {
63
+ rethrowLockError(error, host.stackName);
64
+ throw error;
65
+ }
66
+ step("Refreshing and destroying the stack");
67
+ try {
68
+ await host.stack.destroy({ ...streamOptions(), refresh: true });
69
+ }
70
+ catch (error) {
71
+ rethrowLockError(error, host.stackName);
72
+ throw error;
73
+ }
74
+ }
75
+ /** Previews what {@link createStack} would change, with a full diff. */
76
+ export async function previewStack(host) {
77
+ step("Previewing changes");
78
+ try {
79
+ await host.stack.preview({ ...streamOptions(), diff: true });
80
+ }
81
+ catch (error) {
82
+ rethrowLockError(error, host.stackName);
83
+ throw error;
84
+ }
85
+ }
86
+ /** Releases a stuck state lock left behind by an interrupted operation. */
87
+ export async function cancelStack(host) {
88
+ step("Releasing the state lock");
89
+ await host.stack.cancel();
90
+ }
91
+ /** Prints the stack outputs as a JSON object. */
92
+ export async function printOutputs(host) {
93
+ const outputs = await host.stack.outputs();
94
+ const plain = Object.fromEntries(Object.entries(outputs).map(([key, output]) => [key, output.value]));
95
+ process.stdout.write(`${JSON.stringify(plain, undefined, 2)}\n`);
96
+ }
97
+ async function purgeContainerHostedProviders(host) {
98
+ for (const providerName of host.containerHostedProviders) {
99
+ const purge = await purgeProviderFromState(host.stack, providerName);
100
+ const removed = purge.removedResources + purge.removedPendingOperations;
101
+ if (removed > 0) {
102
+ detail(`Purged ${removed} state entries tied to provider "${providerName}".`);
103
+ }
104
+ }
105
+ }
106
+ function rethrowLockError(error, stackName) {
107
+ if (error instanceof automation.ConcurrentUpdateError) {
108
+ throw new SandboxLockError(stackName);
109
+ }
110
+ }
@@ -0,0 +1,30 @@
1
+ import * as pulumi from "@pulumi/pulumi";
2
+ /** The shape of `T` after every nested Pulumi output and promise has resolved. */
3
+ export type DeepResolved<T> = T extends pulumi.Output<infer U> ? DeepResolved<U> : T extends Promise<infer U> ? DeepResolved<U> : T extends ReadonlyArray<infer E> ? DeepResolved<E>[] : T extends object ? {
4
+ [K in keyof T]: DeepResolved<T[K]>;
5
+ } : T;
6
+ /**
7
+ * Resolves every Pulumi output nested anywhere inside a plain value — arrays,
8
+ * objects, and promises included — into a single output of the fully
9
+ * concrete shape.
10
+ *
11
+ * This is the bridge between resource graphs and host-side side effects:
12
+ * collect generated credentials, ports, and endpoints into one structure,
13
+ * deep-resolve it, and render environment files or developer artifacts in a
14
+ * single `apply`:
15
+ *
16
+ * ```typescript
17
+ * deepResolve({ clientSecret: client.clientSecret, port: 5432 }).apply((resolved) => {
18
+ * env.add(resolved);
19
+ * env.write("application/.env.local");
20
+ * });
21
+ * ```
22
+ *
23
+ * The value must be plain data: primitives, arrays, plain objects, promises,
24
+ * and outputs, nested arbitrarily. Class instances and circular references
25
+ * are rejected with an error naming the offending path — Pulumi's output
26
+ * machinery would otherwise silently flatten an instance into a plain bag
27
+ * of properties.
28
+ */
29
+ export declare function deepResolve<T>(value: T): pulumi.Output<DeepResolved<T>>;
30
+ //# sourceMappingURL=outputs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outputs.d.ts","sourceRoot":"","sources":["../src/outputs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,gBAAgB,CAAC;AAGzC,kFAAkF;AAClF,MAAM,MAAM,YAAY,CAAC,CAAC,IACxB,CAAC,SAAS,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,GAC5B,YAAY,CAAC,CAAC,CAAC,GACf,CAAC,SAAS,OAAO,CAAC,MAAM,CAAC,CAAC,GACxB,YAAY,CAAC,CAAC,CAAC,GACf,CAAC,SAAS,aAAa,CAAC,MAAM,CAAC,CAAC,GAC9B,YAAY,CAAC,CAAC,CAAC,EAAE,GACjB,CAAC,SAAS,MAAM,GACd;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,GACtC,CAAC,CAAC;AAEd;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAEvE"}
@@ -0,0 +1,65 @@
1
+ import * as pulumi from "@pulumi/pulumi";
2
+ import { SandboxError } from "./errors.js";
3
+ /**
4
+ * Resolves every Pulumi output nested anywhere inside a plain value — arrays,
5
+ * objects, and promises included — into a single output of the fully
6
+ * concrete shape.
7
+ *
8
+ * This is the bridge between resource graphs and host-side side effects:
9
+ * collect generated credentials, ports, and endpoints into one structure,
10
+ * deep-resolve it, and render environment files or developer artifacts in a
11
+ * single `apply`:
12
+ *
13
+ * ```typescript
14
+ * deepResolve({ clientSecret: client.clientSecret, port: 5432 }).apply((resolved) => {
15
+ * env.add(resolved);
16
+ * env.write("application/.env.local");
17
+ * });
18
+ * ```
19
+ *
20
+ * The value must be plain data: primitives, arrays, plain objects, promises,
21
+ * and outputs, nested arbitrarily. Class instances and circular references
22
+ * are rejected with an error naming the offending path — Pulumi's output
23
+ * machinery would otherwise silently flatten an instance into a plain bag
24
+ * of properties.
25
+ */
26
+ export function deepResolve(value) {
27
+ return pulumi.output(resolveUnknown(value, "value", new WeakSet()));
28
+ }
29
+ function resolveUnknown(value, path, visiting) {
30
+ if (pulumi.Output.isInstance(value) || value instanceof Promise) {
31
+ return pulumi.output(value).apply((inner) => resolveUnknown(inner, path, visiting));
32
+ }
33
+ if (Array.isArray(value)) {
34
+ enterValue(value, path, visiting);
35
+ const resolved = pulumi.all(value.map((entry, index) => resolveUnknown(entry, `${path}[${index}]`, visiting)));
36
+ visiting.delete(value);
37
+ return resolved;
38
+ }
39
+ if (typeof value === "object" && value !== null) {
40
+ if (!isPlainObject(value)) {
41
+ throw new SandboxError(`deepResolve only accepts plain data, but ${path} is an instance of ${value.constructor?.name ?? "an unknown class"}.`);
42
+ }
43
+ enterValue(value, path, visiting);
44
+ const resolved = {};
45
+ for (const [key, entry] of Object.entries(value)) {
46
+ resolved[key] = resolveUnknown(entry, `${path}.${key}`, visiting);
47
+ }
48
+ visiting.delete(value);
49
+ return pulumi.all(resolved);
50
+ }
51
+ if (typeof value === "function") {
52
+ throw new SandboxError(`deepResolve only accepts plain data, but ${path} is a function.`);
53
+ }
54
+ return value;
55
+ }
56
+ function enterValue(value, path, visiting) {
57
+ if (visiting.has(value)) {
58
+ throw new SandboxError(`deepResolve found a circular reference at ${path}.`);
59
+ }
60
+ visiting.add(value);
61
+ }
62
+ function isPlainObject(value) {
63
+ const prototype = Object.getPrototypeOf(value);
64
+ return prototype === Object.prototype || prototype === null;
65
+ }
@@ -0,0 +1,57 @@
1
+ import * as pulumi from "@pulumi/pulumi";
2
+ export interface HttpProbeOptions {
3
+ /** Give up after this long. Default: 180 seconds. */
4
+ timeoutMs?: number | undefined;
5
+ /** Pause between attempts. Default: 2 seconds. */
6
+ intervalMs?: number | undefined;
7
+ /**
8
+ * What counts as ready. `"any-response"` (the default) accepts every HTTP
9
+ * status — including errors like 403 — because a status line proves the
10
+ * server is up, which is all a boot probe needs. `"ok"` additionally
11
+ * requires a 2xx status.
12
+ */
13
+ expect?: "any-response" | "ok" | undefined;
14
+ }
15
+ /**
16
+ * Polls an HTTP endpoint until it responds, the configured expectation is
17
+ * met, or the timeout elapses. Returns whether the endpoint became ready.
18
+ *
19
+ * Deliberately never throws: on timeout it logs a warning and returns
20
+ * `false`, so lifecycle flows that race a disappearing container (destroy,
21
+ * refresh) degrade to a warning instead of wedging the run. The component
22
+ * that actually depends on the endpoint will surface the real connection
23
+ * error at its own layer.
24
+ */
25
+ export declare function waitForHttp(url: string, options?: HttpProbeOptions): Promise<boolean>;
26
+ export interface ReadyWhenHttpOptions extends HttpProbeOptions {
27
+ /**
28
+ * Runs once the probe succeeds — the place for imperative post-boot
29
+ * configuration (a `docker exec` against the freshly started container,
30
+ * for example). Skipped when the probe times out. Note that the readiness
31
+ * chain re-executes on every update that resolves the gate, so this
32
+ * callback must be idempotent.
33
+ */
34
+ onReady?: (() => void | Promise<void>) | undefined;
35
+ }
36
+ /**
37
+ * An output that resolves to `url` once the endpoint behind it responds,
38
+ * gated on another resource being scheduled first — typically the container
39
+ * that serves the endpoint:
40
+ *
41
+ * ```typescript
42
+ * const adminUrl = readyWhenHttp(keycloakContainer.id, "http://localhost:20080");
43
+ * new keycloak.Provider("keycloak", { url: adminUrl, ... });
44
+ * ```
45
+ *
46
+ * Anything consuming the returned output (a provider, a dependent resource)
47
+ * is therefore held back until the service has actually booted. The polling
48
+ * happens in a plain `apply` closure — nothing is serialized into Pulumi
49
+ * state — and the output always resolves to `url`, even on timeout, to keep
50
+ * the graph healthy during destroy and refresh.
51
+ *
52
+ * Dry runs are exempt: during a preview the output resolves immediately,
53
+ * without probing and without `onReady` — a preview must neither stall on a
54
+ * stopped container nor execute side effects.
55
+ */
56
+ export declare function readyWhenHttp(gate: pulumi.Input<unknown>, url: string, options?: ReadyWhenHttpOptions): pulumi.Output<string>;
57
+ //# sourceMappingURL=readiness.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"readiness.d.ts","sourceRoot":"","sources":["../src/readiness.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,gBAAgB,CAAC;AAGzC,MAAM,WAAW,gBAAgB;IAC/B,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,kDAAkD;IAClD,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,cAAc,GAAG,IAAI,GAAG,SAAS,CAAC;CAC5C;AAKD;;;;;;;;;GASG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,OAAO,CAAC,CAoB/F;AAED,MAAM,WAAW,oBAAqB,SAAQ,gBAAgB;IAC5D;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,SAAS,CAAC;CACpD;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,EAC3B,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,oBAAyB,GACjC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAWvB"}
@@ -0,0 +1,66 @@
1
+ import * as pulumi from "@pulumi/pulumi";
2
+ import { warn } from "./terminal.js";
3
+ const DEFAULT_TIMEOUT_MS = 180_000;
4
+ const DEFAULT_INTERVAL_MS = 2_000;
5
+ /**
6
+ * Polls an HTTP endpoint until it responds, the configured expectation is
7
+ * met, or the timeout elapses. Returns whether the endpoint became ready.
8
+ *
9
+ * Deliberately never throws: on timeout it logs a warning and returns
10
+ * `false`, so lifecycle flows that race a disappearing container (destroy,
11
+ * refresh) degrade to a warning instead of wedging the run. The component
12
+ * that actually depends on the endpoint will surface the real connection
13
+ * error at its own layer.
14
+ */
15
+ export async function waitForHttp(url, options = {}) {
16
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
17
+ const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
18
+ const expect = options.expect ?? "any-response";
19
+ const deadline = Date.now() + timeoutMs;
20
+ while (Date.now() < deadline) {
21
+ try {
22
+ const response = await fetch(url, { signal: AbortSignal.timeout(Math.min(intervalMs * 5, 10_000)) });
23
+ if (expect === "any-response" || response.ok) {
24
+ return true;
25
+ }
26
+ }
27
+ catch {
28
+ // Connection refused or timed out — the server is still booting.
29
+ }
30
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
31
+ }
32
+ warn(`${url} did not respond within ${Math.round(timeoutMs / 1000)}s; continuing anyway.`);
33
+ return false;
34
+ }
35
+ /**
36
+ * An output that resolves to `url` once the endpoint behind it responds,
37
+ * gated on another resource being scheduled first — typically the container
38
+ * that serves the endpoint:
39
+ *
40
+ * ```typescript
41
+ * const adminUrl = readyWhenHttp(keycloakContainer.id, "http://localhost:20080");
42
+ * new keycloak.Provider("keycloak", { url: adminUrl, ... });
43
+ * ```
44
+ *
45
+ * Anything consuming the returned output (a provider, a dependent resource)
46
+ * is therefore held back until the service has actually booted. The polling
47
+ * happens in a plain `apply` closure — nothing is serialized into Pulumi
48
+ * state — and the output always resolves to `url`, even on timeout, to keep
49
+ * the graph healthy during destroy and refresh.
50
+ *
51
+ * Dry runs are exempt: during a preview the output resolves immediately,
52
+ * without probing and without `onReady` — a preview must neither stall on a
53
+ * stopped container nor execute side effects.
54
+ */
55
+ export function readyWhenHttp(gate, url, options = {}) {
56
+ return pulumi.output(gate).apply(async () => {
57
+ if (pulumi.runtime.isDryRun()) {
58
+ return url;
59
+ }
60
+ const ready = await waitForHttp(url, options);
61
+ if (ready && options.onReady) {
62
+ await options.onReady();
63
+ }
64
+ return url;
65
+ });
66
+ }