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.
- package/LICENSE +21 -0
- package/README.md +282 -0
- package/dist/actions.d.ts +7 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +14 -0
- package/dist/backend.d.ts +40 -0
- package/dist/backend.d.ts.map +1 -0
- package/dist/backend.js +40 -0
- package/dist/docker/exec.d.ts +22 -0
- package/dist/docker/exec.d.ts.map +1 -0
- package/dist/docker/exec.js +26 -0
- package/dist/docker/index.d.ts +12 -0
- package/dist/docker/index.d.ts.map +1 -0
- package/dist/docker/index.js +8 -0
- package/dist/docker/mask-volumes.d.ts +74 -0
- package/dist/docker/mask-volumes.d.ts.map +1 -0
- package/dist/docker/mask-volumes.js +97 -0
- package/dist/docker/shell.d.ts +23 -0
- package/dist/docker/shell.d.ts.map +1 -0
- package/dist/docker/shell.js +34 -0
- package/dist/environment-file.d.ts +58 -0
- package/dist/environment-file.d.ts.map +1 -0
- package/dist/environment-file.js +129 -0
- package/dist/errors.d.ts +43 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +47 -0
- package/dist/git.d.ts +11 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +24 -0
- package/dist/identity.d.ts +55 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +94 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/interactive.d.ts +15 -0
- package/dist/interactive.d.ts.map +1 -0
- package/dist/interactive.js +45 -0
- package/dist/lifecycle.d.ts +41 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +110 -0
- package/dist/outputs.d.ts +30 -0
- package/dist/outputs.d.ts.map +1 -0
- package/dist/outputs.js +65 -0
- package/dist/readiness.d.ts +57 -0
- package/dist/readiness.d.ts.map +1 -0
- package/dist/readiness.js +66 -0
- package/dist/sandbox.d.ts +172 -0
- package/dist/sandbox.d.ts.map +1 -0
- package/dist/sandbox.js +260 -0
- package/dist/state-surgery.d.ts +34 -0
- package/dist/state-surgery.d.ts.map +1 -0
- package/dist/state-surgery.js +94 -0
- package/dist/terminal.d.ts +35 -0
- package/dist/terminal.d.ts.map +1 -0
- package/dist/terminal.js +67 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 The pulumi-sandbox contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# Pulumi Sandbox
|
|
2
|
+
|
|
3
|
+
[](https://github.com/cgardev/pulumi-sandbox/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/pulumi-sandbox)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
**Local development sandboxes as code.**
|
|
8
|
+
|
|
9
|
+
Write a plain [Pulumi](https://www.pulumi.com) program describing the local
|
|
10
|
+
infrastructure your application needs — containers, databases, message
|
|
11
|
+
brokers, identity servers — and get a complete, per-developer sandbox
|
|
12
|
+
lifecycle around it. No backend account, no YAML, no glue scripts.
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
// src/sandbox.ts
|
|
16
|
+
import * as docker from "@pulumi/docker";
|
|
17
|
+
import { sandbox } from "pulumi-sandbox";
|
|
18
|
+
|
|
19
|
+
await sandbox({ name: "shop" }, (context) => {
|
|
20
|
+
const image = new docker.RemoteImage("postgres", { name: "postgres:18", keepLocally: true });
|
|
21
|
+
|
|
22
|
+
new docker.Container("database", {
|
|
23
|
+
image: image.imageId,
|
|
24
|
+
name: context.physicalName("database"),
|
|
25
|
+
ports: [{ internal: 5432, external: 25432 }],
|
|
26
|
+
envs: ["POSTGRES_USER=dev", "POSTGRES_PASSWORD=dev", "POSTGRES_DB=shop"],
|
|
27
|
+
mustRun: true,
|
|
28
|
+
}, { deleteBeforeReplace: true });
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
node src/sandbox.ts create # provision (or update) the sandbox
|
|
34
|
+
node src/sandbox.ts destroy # tear it down
|
|
35
|
+
node src/sandbox.ts # interactive menu
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
That is the entire setup. State lives in a git-ignored `.sandbox/` directory
|
|
39
|
+
on the local `file://` backend; with Node.js 24+ the TypeScript entry point
|
|
40
|
+
runs directly, no build step involved.
|
|
41
|
+
|
|
42
|
+
## Why not docker-compose?
|
|
43
|
+
|
|
44
|
+
A compose file describes containers. A sandbox program describes an
|
|
45
|
+
*environment*: it can wait for a server to boot before configuring realms
|
|
46
|
+
inside it, generate credentials and render them into the `.env` files your
|
|
47
|
+
applications load, derive a container per module of your repository, and
|
|
48
|
+
reuse every Pulumi provider in existence. With the full expressiveness of
|
|
49
|
+
TypeScript — loops, functions, composition — complex topologies (one database
|
|
50
|
+
per service, port plans, cross-service wiring) stay readable.
|
|
51
|
+
|
|
52
|
+
This library supplies everything *around* that program, so a project's
|
|
53
|
+
infrastructure entry point contains nothing but infrastructure.
|
|
54
|
+
|
|
55
|
+
## What you get
|
|
56
|
+
|
|
57
|
+
- **Zero-configuration state.** A self-contained `file://` backend under
|
|
58
|
+
`.sandbox/` — no Pulumi account, no cloud bucket, nothing to log into.
|
|
59
|
+
Point `backendUrl` at `s3://...` later if the team wants shared state.
|
|
60
|
+
- **Per-developer isolation.** A developer id (from `SANDBOX_DEV_ID`, an
|
|
61
|
+
optional `.env` file, or the `local` default) suffixes the stack and every
|
|
62
|
+
physical resource name, so two developers on one machine — or two checkouts
|
|
63
|
+
given distinct `SANDBOX_DEV_ID` values — never collide.
|
|
64
|
+
- **A complete lifecycle.** `create`, `destroy`, `reset`, `preview`,
|
|
65
|
+
`cancel`, `outputs`, `help`, an interactive menu, and your own custom
|
|
66
|
+
commands — with readable output and honest exit codes.
|
|
67
|
+
- **A lifecycle that survives reality.** Refresh is folded into `create` and
|
|
68
|
+
`destroy`, so containers killed by hand drop out of state instead of
|
|
69
|
+
failing the run. Providers hosted *inside* sandbox-managed containers
|
|
70
|
+
(Keycloak realms, database schemas) get state surgery on destroy and a
|
|
71
|
+
purge-and-retry on create — see below.
|
|
72
|
+
- **Developer-experience helpers.** `EnvironmentFile` renders ordered,
|
|
73
|
+
grouped `.env` files; `deepResolve` turns a tree of Pulumi outputs into one
|
|
74
|
+
concrete value; `readyWhenHttp` gates providers on a service actually
|
|
75
|
+
booting; `findGitRoot` anchors paths; `pulumi-sandbox/docker` adds
|
|
76
|
+
`attachShell`, `dockerExec`, and rule-driven mask-volume discovery.
|
|
77
|
+
- **A tiny, generic core.** ESM, fully typed, zero runtime dependencies, and
|
|
78
|
+
`@pulumi/pulumi` as the only peer dependency. The library has no knowledge
|
|
79
|
+
of any particular database, build tool, or identity server — your program
|
|
80
|
+
and your rules carry the specifics.
|
|
81
|
+
|
|
82
|
+
## Requirements
|
|
83
|
+
|
|
84
|
+
- Node.js >= 24
|
|
85
|
+
- The [Pulumi CLI](https://www.pulumi.com/docs/install/) on the PATH (no account needed)
|
|
86
|
+
- Docker, when the program manages containers
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pnpm add pulumi-sandbox @pulumi/pulumi
|
|
90
|
+
# plus the providers your program uses, e.g. for containers:
|
|
91
|
+
pnpm add @pulumi/docker
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## The lifecycle
|
|
95
|
+
|
|
96
|
+
| Action | What happens |
|
|
97
|
+
|:-----------|:----------------------------------------------------------------------------------------------|
|
|
98
|
+
| `create` | `up` with refresh folded in; on failure, purge container-hosted provider state and retry once |
|
|
99
|
+
| `destroy` | Purge container-hosted provider state, then `destroy` with refresh folded in |
|
|
100
|
+
| `reset` | `destroy` followed by `create`, in one process |
|
|
101
|
+
| `preview` | Diffed preview of what `create` would change |
|
|
102
|
+
| `cancel` | Release a stuck state lock left by an interrupted run |
|
|
103
|
+
| `outputs` | Print the stack outputs as JSON |
|
|
104
|
+
| *(none)* | Interactive menu over all of the above |
|
|
105
|
+
|
|
106
|
+
A concurrent-update collision is reported as a hint to run `cancel`, never as
|
|
107
|
+
a stack trace — and a `reset` whose destroy half hits the lock stops instead
|
|
108
|
+
of silently proceeding.
|
|
109
|
+
|
|
110
|
+
## The context
|
|
111
|
+
|
|
112
|
+
The program receives a context describing the run:
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
await sandbox({ name: "shop" }, (context) => {
|
|
116
|
+
context.devId; // "jdoe" — the resolved developer id
|
|
117
|
+
context.stackName; // "shop-jdoe"
|
|
118
|
+
context.physicalName("orders-db"); // "shop-orders-db-jdoe"
|
|
119
|
+
context.action; // the lifecycle operation executing the program
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`physicalName` keeps container, network, and volume names collision-free per
|
|
124
|
+
developer. `action` is the operation currently executing the program —
|
|
125
|
+
`create` or `preview` — and gates side effects like writing generated
|
|
126
|
+
artifacts. The program only runs for operations that need the resource
|
|
127
|
+
graph; a `destroy` works from the recorded state and never executes it, so
|
|
128
|
+
programs need no destroy-time guards.
|
|
129
|
+
|
|
130
|
+
Returning a record from the program publishes it as stack outputs:
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
await sandbox({ name: "shop" }, () => {
|
|
134
|
+
return { adminUrl: "http://localhost:25080" };
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Container-hosted providers
|
|
139
|
+
|
|
140
|
+
Some providers manage resources *inside* a container the sandbox itself
|
|
141
|
+
runs — realms inside a Keycloak container, schemas inside a database
|
|
142
|
+
container. Pulumi treats those resources as independent of the container, so
|
|
143
|
+
when the container disappears (a destroy, or a developer's `docker rm`), any
|
|
144
|
+
refresh, update, or destroy aborts while initializing a provider whose
|
|
145
|
+
service no longer exists.
|
|
146
|
+
|
|
147
|
+
Declare such providers and the lifecycle handles the rest:
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
await sandbox(
|
|
151
|
+
{ name: "shop", containerHostedProviders: ["identity"] },
|
|
152
|
+
() => {
|
|
153
|
+
const identity = new IdentityServer(/* keycloak container + sidecar */);
|
|
154
|
+
|
|
155
|
+
const provider = new keycloak.Provider("identity", {
|
|
156
|
+
url: identity.readyUrl, // configures itself only after boot
|
|
157
|
+
// ...
|
|
158
|
+
});
|
|
159
|
+
new keycloak.Realm("shop", { realm: "shop" }, {
|
|
160
|
+
provider,
|
|
161
|
+
retainOnDelete: true, // safety net for partial destroys
|
|
162
|
+
deletedWith: identity.container, // documents the dependency
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
);
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
On `destroy`, the provider and everything it manages are removed from state
|
|
169
|
+
before the plan runs — the container teardown wipes them physically, so
|
|
170
|
+
nothing needs to talk to the doomed service. On `create`, a failed update
|
|
171
|
+
triggers the same purge and a single retry, which recovers sandboxes whose
|
|
172
|
+
containers were removed out-of-band. The mechanism is generic: any provider
|
|
173
|
+
resource name can be listed, and `purgeProviderFromState` is exported for
|
|
174
|
+
custom flows.
|
|
175
|
+
|
|
176
|
+
## Rendering configuration for applications
|
|
177
|
+
|
|
178
|
+
Sandboxes exist so applications can run against them. `EnvironmentFile`
|
|
179
|
+
accumulates variables in ordered, blank-line-separated groups; `deepResolve`
|
|
180
|
+
collapses any tree of Pulumi outputs into one concrete value, so generated
|
|
181
|
+
credentials land in the same file as static ports:
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
import { EnvironmentFile, deepResolve } from "pulumi-sandbox";
|
|
185
|
+
|
|
186
|
+
const environment = new EnvironmentFile([
|
|
187
|
+
{ ORDERS_DATABASE_URL: ordersDatabase.connectionUri },
|
|
188
|
+
{ SMTP_HOST: "localhost", SMTP_PORT: 25025 },
|
|
189
|
+
]);
|
|
190
|
+
|
|
191
|
+
if (context.action === "create") {
|
|
192
|
+
deepResolve({ secret: ordersApi.clientSecret }).apply(({ secret }) => {
|
|
193
|
+
environment.add({ OIDC_CLIENT_SECRET: secret });
|
|
194
|
+
environment.write("generated/orders.env");
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
An `undefined` or `null` value throws immediately with the offending key —
|
|
200
|
+
a loud failure beats a poisoned environment file.
|
|
201
|
+
|
|
202
|
+
## Custom commands
|
|
203
|
+
|
|
204
|
+
Verbs beyond the lifecycle dispatch before any Pulumi machinery starts, so
|
|
205
|
+
they stay instant:
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
import { attachShell } from "pulumi-sandbox/docker";
|
|
209
|
+
|
|
210
|
+
await sandbox(
|
|
211
|
+
{
|
|
212
|
+
name: "workspace",
|
|
213
|
+
commands: {
|
|
214
|
+
shell: {
|
|
215
|
+
description: "Open a shell inside the workspace container",
|
|
216
|
+
run: ({ physicalName, argv }) => attachShell(physicalName("dev"), { shell: argv[0] }),
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
program,
|
|
221
|
+
);
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Examples
|
|
225
|
+
|
|
226
|
+
| Example | Shows |
|
|
227
|
+
|:-----------------------------------------------|:----------------------------------------------------------------------------------------|
|
|
228
|
+
| [`getting-started`](examples/getting-started) | One database, one generated `.env` — the minimal loop |
|
|
229
|
+
| [`multi-service`](examples/multi-service) | Databases per service, mail catcher, Keycloak realm via a container-hosted provider |
|
|
230
|
+
| [`dev-workspace`](examples/dev-workspace) | A containerized development environment with rule-discovered mask volumes and a `shell` command |
|
|
231
|
+
|
|
232
|
+
## State layout and portability
|
|
233
|
+
|
|
234
|
+
```
|
|
235
|
+
.sandbox/ # add to .gitignore
|
|
236
|
+
├── state/ # the file:// backend: checkpoints, history, backups
|
|
237
|
+
└── work/<project>/ # the generated Pulumi project and per-stack settings
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
By default `.sandbox/` lives in the package containing the entry script —
|
|
241
|
+
anchored there rather than to the working directory, so invoking the sandbox
|
|
242
|
+
from anywhere targets the same state. Override the location with `homeDir`.
|
|
243
|
+
|
|
244
|
+
Secrets on the local backend are encrypted with a well-known default
|
|
245
|
+
passphrase (`sandbox`) to keep the zero-configuration promise — local
|
|
246
|
+
sandboxes hold throwaway development credentials. Override `passphrase` or
|
|
247
|
+
set `PULUMI_CONFIG_PASSPHRASE` when pointing at a shared backend.
|
|
248
|
+
|
|
249
|
+
The `file://` URL is built in the one form Pulumi's DIY backend accepts on
|
|
250
|
+
both Windows and POSIX (`fileBackendUrl`), so the same entry point works for
|
|
251
|
+
the whole team.
|
|
252
|
+
|
|
253
|
+
## API overview
|
|
254
|
+
|
|
255
|
+
Core (`pulumi-sandbox`):
|
|
256
|
+
|
|
257
|
+
- `sandbox(options, program)` — the complete entry point: dispatch, lifecycle, error rendering
|
|
258
|
+
- `createSandbox(options, program)` / `Sandbox` — programmatic control, the underlying `automation.Stack` included
|
|
259
|
+
- `EnvironmentFile`, `deepResolve`, `waitForHttp`, `readyWhenHttp`, `findGitRoot`
|
|
260
|
+
- `resolveDevId`, `parseEnvFile`, `readEnvFile`, `booleanFlag`
|
|
261
|
+
- `purgeProviderFromState`, `removeProviderFromDeployment`
|
|
262
|
+
- `fileBackendUrl`, `resolveDirectories`, `ensureDirectories`
|
|
263
|
+
- `SandboxError`, `SandboxConfigurationError`, `SandboxLockError`, `EnvironmentFileError`
|
|
264
|
+
|
|
265
|
+
Docker utilities (`pulumi-sandbox/docker`, host-side, no `@pulumi/docker` required):
|
|
266
|
+
|
|
267
|
+
- `attachShell(containerName, options)` — interactive `docker exec`
|
|
268
|
+
- `dockerExec(containerName, command, options)` — idempotent post-boot configuration
|
|
269
|
+
- `discoverMaskVolumes(root, { containerRoot, rules })` — rule-driven discovery of directories to mask with container-local volumes
|
|
270
|
+
|
|
271
|
+
## Development
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
pnpm install
|
|
275
|
+
pnpm build # compile to dist/
|
|
276
|
+
pnpm check # type-check sources and tests
|
|
277
|
+
pnpm test # vitest
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## License
|
|
281
|
+
|
|
282
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Lifecycle verbs understood by every sandbox. */
|
|
2
|
+
export declare const LIFECYCLE_ACTIONS: readonly ["create", "destroy", "reset", "preview", "cancel", "outputs"];
|
|
3
|
+
export type LifecycleAction = (typeof LIFECYCLE_ACTIONS)[number];
|
|
4
|
+
/** One-line description per lifecycle verb, shared by `help` and the interactive menu. */
|
|
5
|
+
export declare const ACTION_DESCRIPTIONS: Record<LifecycleAction, string>;
|
|
6
|
+
export declare function isLifecycleAction(verb: string): verb is LifecycleAction;
|
|
7
|
+
//# sourceMappingURL=actions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"AAAA,mDAAmD;AACnD,eAAO,MAAM,iBAAiB,yEAA0E,CAAC;AAEzG,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjE,0FAA0F;AAC1F,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,eAAe,EAAE,MAAM,CAO/D,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,IAAI,eAAe,CAEvE"}
|
package/dist/actions.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Lifecycle verbs understood by every sandbox. */
|
|
2
|
+
export const LIFECYCLE_ACTIONS = ["create", "destroy", "reset", "preview", "cancel", "outputs"];
|
|
3
|
+
/** One-line description per lifecycle verb, shared by `help` and the interactive menu. */
|
|
4
|
+
export const ACTION_DESCRIPTIONS = {
|
|
5
|
+
create: "Provision the sandbox, or update it to match the program",
|
|
6
|
+
destroy: "Tear down every resource the sandbox manages",
|
|
7
|
+
reset: "Destroy, then create — a clean slate in one command",
|
|
8
|
+
preview: "Show what create would change, without changing it",
|
|
9
|
+
cancel: "Release a stuck state lock left by an interrupted run",
|
|
10
|
+
outputs: "Print the stack outputs as JSON",
|
|
11
|
+
};
|
|
12
|
+
export function isLifecycleAction(verb) {
|
|
13
|
+
return LIFECYCLE_ACTIONS.includes(verb);
|
|
14
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the `file://` URL for Pulumi's DIY (self-managed) backend from an
|
|
3
|
+
* absolute directory path.
|
|
4
|
+
*
|
|
5
|
+
* Node's `pathToFileURL` produces a triple-slash URL (`file:///D:/...`) that
|
|
6
|
+
* the DIY backend mis-parses on Windows — it re-prepends the drive, yielding
|
|
7
|
+
* `file:///D:/D:/...` and a "filename ... syntax is incorrect" error. The
|
|
8
|
+
* form Pulumi accepts on Windows is `file://D:/forward/slash/path`.
|
|
9
|
+
* Prefixing the absolute path (with forward slashes) with `file://` gives
|
|
10
|
+
* exactly that on Windows and the correct `file:///absolute/path` on POSIX,
|
|
11
|
+
* where the path already starts with `/`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function fileBackendUrl(absoluteDirectory: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* On-disk layout of a local sandbox. Everything lives under a single home
|
|
16
|
+
* directory (default `.sandbox/`, git-ignored) so a checkout can be cleaned
|
|
17
|
+
* with one deletion and nothing machine-specific is ever committed.
|
|
18
|
+
*/
|
|
19
|
+
export interface SandboxDirectories {
|
|
20
|
+
/** Root of the sandbox-managed files. */
|
|
21
|
+
home: string;
|
|
22
|
+
/** Pulumi DIY backend storage — checkpoints, history, backups, locks. */
|
|
23
|
+
state: string;
|
|
24
|
+
/** Pulumi work directory — the generated project and per-stack settings. */
|
|
25
|
+
work: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Resolves the directory layout under the given sandbox home directory. The
|
|
29
|
+
* work directory is scoped per project so two sandboxes sharing a home never
|
|
30
|
+
* fight over the generated project file; the state directory is shared, as
|
|
31
|
+
* the DIY backend already namespaces checkpoints by project.
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolveDirectories(homeDirectory: string, projectName: string): SandboxDirectories;
|
|
34
|
+
/**
|
|
35
|
+
* Creates the sandbox directories. Both must exist before the Pulumi
|
|
36
|
+
* workspace is constructed: the file backend needs somewhere to write its
|
|
37
|
+
* checkpoint, and the work directory receives the generated project file.
|
|
38
|
+
*/
|
|
39
|
+
export declare function ensureDirectories(directories: SandboxDirectories): void;
|
|
40
|
+
//# sourceMappingURL=backend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,iBAAiB,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,KAAK,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,kBAAkB,CAOjG;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,kBAAkB,GAAG,IAAI,CAGvE"}
|
package/dist/backend.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
/**
|
|
4
|
+
* Builds the `file://` URL for Pulumi's DIY (self-managed) backend from an
|
|
5
|
+
* absolute directory path.
|
|
6
|
+
*
|
|
7
|
+
* Node's `pathToFileURL` produces a triple-slash URL (`file:///D:/...`) that
|
|
8
|
+
* the DIY backend mis-parses on Windows — it re-prepends the drive, yielding
|
|
9
|
+
* `file:///D:/D:/...` and a "filename ... syntax is incorrect" error. The
|
|
10
|
+
* form Pulumi accepts on Windows is `file://D:/forward/slash/path`.
|
|
11
|
+
* Prefixing the absolute path (with forward slashes) with `file://` gives
|
|
12
|
+
* exactly that on Windows and the correct `file:///absolute/path` on POSIX,
|
|
13
|
+
* where the path already starts with `/`.
|
|
14
|
+
*/
|
|
15
|
+
export function fileBackendUrl(absoluteDirectory) {
|
|
16
|
+
return `file://${absoluteDirectory.replace(/\\/g, "/")}`;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Resolves the directory layout under the given sandbox home directory. The
|
|
20
|
+
* work directory is scoped per project so two sandboxes sharing a home never
|
|
21
|
+
* fight over the generated project file; the state directory is shared, as
|
|
22
|
+
* the DIY backend already namespaces checkpoints by project.
|
|
23
|
+
*/
|
|
24
|
+
export function resolveDirectories(homeDirectory, projectName) {
|
|
25
|
+
const home = path.resolve(homeDirectory);
|
|
26
|
+
return {
|
|
27
|
+
home,
|
|
28
|
+
state: path.join(home, "state"),
|
|
29
|
+
work: path.join(home, "work", projectName),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Creates the sandbox directories. Both must exist before the Pulumi
|
|
34
|
+
* workspace is constructed: the file backend needs somewhere to write its
|
|
35
|
+
* checkpoint, and the work directory receives the generated project file.
|
|
36
|
+
*/
|
|
37
|
+
export function ensureDirectories(directories) {
|
|
38
|
+
fs.mkdirSync(directories.state, { recursive: true });
|
|
39
|
+
fs.mkdirSync(directories.work, { recursive: true });
|
|
40
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface DockerExecOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Warn and return `false` instead of throwing when the command fails.
|
|
4
|
+
* This is the right mode for post-boot configuration hooks: the service
|
|
5
|
+
* that actually needs the configuration will surface a clear error at its
|
|
6
|
+
* own layer, while a throw here would wedge destroy and refresh flows.
|
|
7
|
+
* Default: `true`.
|
|
8
|
+
*/
|
|
9
|
+
warnOnly?: boolean | undefined;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Runs a command inside a running container via `docker exec`, for
|
|
13
|
+
* imperative post-boot configuration that no provider covers — unlocking an
|
|
14
|
+
* admin API, creating a seed user, flipping a development-only setting.
|
|
15
|
+
* Returns whether the command succeeded.
|
|
16
|
+
*
|
|
17
|
+
* Commands must be idempotent: readiness chains re-execute on every Pulumi
|
|
18
|
+
* operation, so the same configuration may run against an already-configured
|
|
19
|
+
* container.
|
|
20
|
+
*/
|
|
21
|
+
export declare function dockerExec(containerName: string, command: readonly string[], options?: DockerExecOptions): boolean;
|
|
22
|
+
//# sourceMappingURL=exec.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exec.d.ts","sourceRoot":"","sources":["../../src/docker/exec.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,iBAAiB;IAChC;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAChC;AAED;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAatH"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { warn } from "../terminal.js";
|
|
3
|
+
/**
|
|
4
|
+
* Runs a command inside a running container via `docker exec`, for
|
|
5
|
+
* imperative post-boot configuration that no provider covers — unlocking an
|
|
6
|
+
* admin API, creating a seed user, flipping a development-only setting.
|
|
7
|
+
* Returns whether the command succeeded.
|
|
8
|
+
*
|
|
9
|
+
* Commands must be idempotent: readiness chains re-execute on every Pulumi
|
|
10
|
+
* operation, so the same configuration may run against an already-configured
|
|
11
|
+
* container.
|
|
12
|
+
*/
|
|
13
|
+
export function dockerExec(containerName, command, options = {}) {
|
|
14
|
+
try {
|
|
15
|
+
execFileSync("docker", ["exec", containerName, ...command], { stdio: "pipe" });
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
const detail = error.stderr?.toString().trim() || error.message;
|
|
20
|
+
if (options.warnOnly ?? true) {
|
|
21
|
+
warn(`docker exec in ${containerName} failed: ${detail}`);
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-side docker utilities — attaching shells, executing post-boot
|
|
3
|
+
* configuration, and discovering mask volumes. Nothing in this module
|
|
4
|
+
* touches Pulumi; it complements the resources a program declares.
|
|
5
|
+
*/
|
|
6
|
+
export { attachShell } from "./shell.js";
|
|
7
|
+
export type { AttachShellOptions } from "./shell.js";
|
|
8
|
+
export { dockerExec } from "./exec.js";
|
|
9
|
+
export type { DockerExecOptions } from "./exec.js";
|
|
10
|
+
export { discoverMaskVolumes } from "./mask-volumes.js";
|
|
11
|
+
export type { MaskVolume, MaskRule, DiscoverMaskVolumesOptions } from "./mask-volumes.js";
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/docker/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,YAAY,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAErD,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,YAAY,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAEnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,0BAA0B,EAAE,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-side docker utilities — attaching shells, executing post-boot
|
|
3
|
+
* configuration, and discovering mask volumes. Nothing in this module
|
|
4
|
+
* touches Pulumi; it complements the resources a program declares.
|
|
5
|
+
*/
|
|
6
|
+
export { attachShell } from "./shell.js";
|
|
7
|
+
export { dockerExec } from "./exec.js";
|
|
8
|
+
export { discoverMaskVolumes } from "./mask-volumes.js";
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A named volume layered over a bind mount, identified by a stable key. The
|
|
3
|
+
* key is meant to be combined with a per-developer volume prefix into the
|
|
4
|
+
* physical volume name.
|
|
5
|
+
*/
|
|
6
|
+
export interface MaskVolume {
|
|
7
|
+
/** Stable, human-readable identifier derived from the rule and the repository path. */
|
|
8
|
+
key: string;
|
|
9
|
+
/** Where the volume mounts inside the container. */
|
|
10
|
+
containerPath: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Declares which directories a project tree generates locally and must
|
|
14
|
+
* therefore be masked. A rule fires in every walked directory containing one
|
|
15
|
+
* of its marker files; the rule's `mask` entries (and whatever `expand`
|
|
16
|
+
* derives from the marker's content) become container-local volumes.
|
|
17
|
+
*
|
|
18
|
+
* The library has no knowledge of any build tool — rules carry all of it.
|
|
19
|
+
* A JVM-and-Node repository, for example, is fully described by:
|
|
20
|
+
*
|
|
21
|
+
* ```typescript
|
|
22
|
+
* const rules: MaskRule[] = [
|
|
23
|
+
* { prefix: "build", markers: ["build.gradle.kts", "settings.gradle.kts"], mask: ["build"] },
|
|
24
|
+
* { prefix: "gradle", markers: ["settings.gradle.kts"], mask: [".gradle"] },
|
|
25
|
+
* { prefix: "node-modules", markers: ["package.json"], mask: ["node_modules"] },
|
|
26
|
+
* ];
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export interface MaskRule {
|
|
30
|
+
/** Prefix for the volume keys this rule produces. */
|
|
31
|
+
prefix: string;
|
|
32
|
+
/** File names whose presence marks a directory as governed by this rule. */
|
|
33
|
+
markers: readonly string[];
|
|
34
|
+
/** Directories to mask, relative to the marked directory. */
|
|
35
|
+
mask: readonly string[];
|
|
36
|
+
/**
|
|
37
|
+
* Optional expansion hook: derives additional directories to mask from the
|
|
38
|
+
* content of the matched marker file — for build systems whose
|
|
39
|
+
* configuration references sibling project roots. Returned paths are
|
|
40
|
+
* resolved against the marker's directory and must stay inside the walked
|
|
41
|
+
* root; anything outside the bind mount cannot be masked and is skipped
|
|
42
|
+
* with a warning.
|
|
43
|
+
*/
|
|
44
|
+
expand?: ((markerPath: string, content: string) => readonly string[]) | undefined;
|
|
45
|
+
}
|
|
46
|
+
export interface DiscoverMaskVolumesOptions {
|
|
47
|
+
/** Where the repository is bind-mounted inside the container, e.g. `/workspace`. */
|
|
48
|
+
containerRoot: string;
|
|
49
|
+
/** The rules describing which directories to mask. */
|
|
50
|
+
rules: readonly MaskRule[];
|
|
51
|
+
/**
|
|
52
|
+
* Directory names never descended into, on top of the directories actually
|
|
53
|
+
* masked and dot-directories (always skipped). Masking already prevents
|
|
54
|
+
* descending into a masked directory, but only where its rule fired — an
|
|
55
|
+
* unrelated directory that merely shares the name is still walked.
|
|
56
|
+
*/
|
|
57
|
+
prune?: readonly string[] | undefined;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Walks a repository and returns one container-local mask volume for every
|
|
61
|
+
* directory the given rules mark as locally generated — directories whose
|
|
62
|
+
* contents must not round-trip between the host bind mount and the
|
|
63
|
+
* container, which is essential when host and container run different
|
|
64
|
+
* platforms (a Windows host building inside a Linux container, for example).
|
|
65
|
+
*
|
|
66
|
+
* Detection keys off the marker files — which exist from the first
|
|
67
|
+
* checkout — never off the masked directories themselves, so a directory
|
|
68
|
+
* that does not exist yet is still masked: docker creates the empty volume
|
|
69
|
+
* on first start and the first in-container build writes there, never back
|
|
70
|
+
* to the host. Add a module to the repository and its masks appear on the
|
|
71
|
+
* next `create`, with no list to maintain.
|
|
72
|
+
*/
|
|
73
|
+
export declare function discoverMaskVolumes(rootDirectory: string, options: DiscoverMaskVolumesOptions): MaskVolume[];
|
|
74
|
+
//# sourceMappingURL=mask-volumes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mask-volumes.d.ts","sourceRoot":"","sources":["../../src/docker/mask-volumes.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,uFAAuF;IACvF,GAAG,EAAE,MAAM,CAAC;IACZ,oDAAoD;IACpD,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,QAAQ;IACvB,qDAAqD;IACrD,MAAM,EAAE,MAAM,CAAC;IAEf,4EAA4E;IAC5E,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAE3B,6DAA6D;IAC7D,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IAExB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,CAAC,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,SAAS,MAAM,EAAE,CAAC,GAAG,SAAS,CAAC;CACnF;AAED,MAAM,WAAW,0BAA0B;IACzC,oFAAoF;IACpF,aAAa,EAAE,MAAM,CAAC;IAEtB,sDAAsD;IACtD,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;IAE3B;;;;;OAKG;IACH,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;CACvC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,0BAA0B,GAAG,UAAU,EAAE,CAgF5G"}
|