@molecule/api-code-sandbox-docker 1.0.1 → 1.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/README.md +92 -2
- package/dist/archive.d.ts +49 -0
- package/dist/archive.d.ts.map +1 -0
- package/dist/archive.js +61 -0
- package/dist/capacity.d.ts +71 -0
- package/dist/capacity.d.ts.map +1 -0
- package/dist/capacity.js +98 -0
- package/dist/egress.d.ts +49 -0
- package/dist/egress.d.ts.map +1 -0
- package/dist/egress.js +424 -0
- package/dist/hibernate.d.ts +79 -0
- package/dist/hibernate.d.ts.map +1 -0
- package/dist/hibernate.js +142 -0
- package/dist/index.d.ts +44 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +44 -1
- package/dist/inspect.d.ts +131 -0
- package/dist/inspect.d.ts.map +1 -0
- package/dist/inspect.js +212 -0
- package/dist/provider.d.ts +14 -0
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +496 -19
- package/dist/request.d.ts +66 -0
- package/dist/request.d.ts.map +1 -0
- package/dist/request.js +56 -0
- package/dist/templates.d.ts +109 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +381 -0
- package/dist/types.d.ts +29 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -26,7 +26,50 @@
|
|
|
26
26
|
* container/volume size (it needs specific storage drivers — e.g. overlay2 on xfs
|
|
27
27
|
* with `pquota` — and errors on the common overlay2/ext4 host), so this provider
|
|
28
28
|
* enforces none. The core `resources.diskMB` is accepted but not applied here; cap
|
|
29
|
-
* disk at the host / volume level instead.
|
|
29
|
+
* disk at the host / volume level instead. `setResources({ diskMB })` THROWS
|
|
30
|
+
* rather than accepting a ceiling it cannot enforce.
|
|
31
|
+
*
|
|
32
|
+
* **Templates are not `commit` alone, and that is not an optimization.**
|
|
33
|
+
* `commitTemplate` copies the caller-named `capturePaths` into a throwaway,
|
|
34
|
+
* volume-less container and commits THAT. A sandbox keeps the project on a
|
|
35
|
+
* volume, and a volume is not part of a container's writable layer — so
|
|
36
|
+
* committing a sandbox directly yields an image that builds, tags, and boots
|
|
37
|
+
* with an empty workspace, with no error anywhere. The throwaway container
|
|
38
|
+
* handles tenant-authored bytes and is committed into something other tenants
|
|
39
|
+
* boot, so it runs with `--network none`, `CapDrop: ALL` plus the three
|
|
40
|
+
* capabilities extraction needs, a memory cap and a process cap, and setuid/setgid
|
|
41
|
+
* bits are stripped from the extracted tree afterwards.
|
|
42
|
+
*
|
|
43
|
+
* Templates live in their OWN repository (`templateRepository`, default
|
|
44
|
+
* `molecule-sandbox-template`) rather than sharing the base image's, so nothing
|
|
45
|
+
* written to sweep templates can reach the base image. `publishTemplate` /
|
|
46
|
+
* `fetchTemplate` use a Docker registry and require `templateRegistry`; without
|
|
47
|
+
* it they THROW rather than returning "not found", because "there is no shared
|
|
48
|
+
* store" and "the shared store does not have it" are different answers.
|
|
49
|
+
*
|
|
50
|
+
* **`hibernate()`/`resume()` report which mechanism ran.** A CRIU checkpoint
|
|
51
|
+
* preserves the process tree; a stop does not, and whether CRIU is available is a
|
|
52
|
+
* property of the host (the daemon's experimental flag plus the `criu` package),
|
|
53
|
+
* discovered at runtime. A sandbox that runs its dev servers as detached exec
|
|
54
|
+
* processes comes back from a stop-style wake alive and serving nothing, so
|
|
55
|
+
* callers must branch on `processesPreserved` — never on `status`, which says
|
|
56
|
+
* `running` either way. The first response that shows CRIU is unavailable
|
|
57
|
+
* disables it for the rest of the process.
|
|
58
|
+
*
|
|
59
|
+
* **`find()` costs one request per match.** Docker's container LIST omits
|
|
60
|
+
* `State.StartedAt`, which is the only field distinguishing an interrupted
|
|
61
|
+
* creation (`startedAt: null` — holds storage forever, will never run) from an
|
|
62
|
+
* ordinary stopped sandbox, so each match is then inspected. Narrow with
|
|
63
|
+
* `labels`; an unfiltered query on a busy host inspects every container.
|
|
64
|
+
*
|
|
65
|
+
* **`capacity()` measures THIS host, so it reports nothing for a TCP daemon.**
|
|
66
|
+
* Over a `tcp://` endpoint the daemon writes to a different machine's storage,
|
|
67
|
+
* and returning this machine's numbers would be a confident wrong answer — so it
|
|
68
|
+
* returns empty `headroom` and says why. On a local socket it reports free bytes
|
|
69
|
+
* and free inodes on the daemon's `DockerRootDir` plus `MemAvailable`. Inodes are
|
|
70
|
+
* omitted (not reported as zero) on filesystems with dynamic inode allocation.
|
|
71
|
+
* `admits` is always `null`: Docker has no quota of its own to consult, so the
|
|
72
|
+
* floors and the refusal stay with the caller.
|
|
30
73
|
*
|
|
31
74
|
* @module
|
|
32
75
|
*/
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0EG;AAEH,cAAc,oBAAoB,CAAA;AAClC,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAE1B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAA;AAMjE;;GAEG;AACH,eAAO,MAAM,QAAQ,EAAE,eAUrB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -26,7 +26,50 @@
|
|
|
26
26
|
* container/volume size (it needs specific storage drivers — e.g. overlay2 on xfs
|
|
27
27
|
* with `pquota` — and errors on the common overlay2/ext4 host), so this provider
|
|
28
28
|
* enforces none. The core `resources.diskMB` is accepted but not applied here; cap
|
|
29
|
-
* disk at the host / volume level instead.
|
|
29
|
+
* disk at the host / volume level instead. `setResources({ diskMB })` THROWS
|
|
30
|
+
* rather than accepting a ceiling it cannot enforce.
|
|
31
|
+
*
|
|
32
|
+
* **Templates are not `commit` alone, and that is not an optimization.**
|
|
33
|
+
* `commitTemplate` copies the caller-named `capturePaths` into a throwaway,
|
|
34
|
+
* volume-less container and commits THAT. A sandbox keeps the project on a
|
|
35
|
+
* volume, and a volume is not part of a container's writable layer — so
|
|
36
|
+
* committing a sandbox directly yields an image that builds, tags, and boots
|
|
37
|
+
* with an empty workspace, with no error anywhere. The throwaway container
|
|
38
|
+
* handles tenant-authored bytes and is committed into something other tenants
|
|
39
|
+
* boot, so it runs with `--network none`, `CapDrop: ALL` plus the three
|
|
40
|
+
* capabilities extraction needs, a memory cap and a process cap, and setuid/setgid
|
|
41
|
+
* bits are stripped from the extracted tree afterwards.
|
|
42
|
+
*
|
|
43
|
+
* Templates live in their OWN repository (`templateRepository`, default
|
|
44
|
+
* `molecule-sandbox-template`) rather than sharing the base image's, so nothing
|
|
45
|
+
* written to sweep templates can reach the base image. `publishTemplate` /
|
|
46
|
+
* `fetchTemplate` use a Docker registry and require `templateRegistry`; without
|
|
47
|
+
* it they THROW rather than returning "not found", because "there is no shared
|
|
48
|
+
* store" and "the shared store does not have it" are different answers.
|
|
49
|
+
*
|
|
50
|
+
* **`hibernate()`/`resume()` report which mechanism ran.** A CRIU checkpoint
|
|
51
|
+
* preserves the process tree; a stop does not, and whether CRIU is available is a
|
|
52
|
+
* property of the host (the daemon's experimental flag plus the `criu` package),
|
|
53
|
+
* discovered at runtime. A sandbox that runs its dev servers as detached exec
|
|
54
|
+
* processes comes back from a stop-style wake alive and serving nothing, so
|
|
55
|
+
* callers must branch on `processesPreserved` — never on `status`, which says
|
|
56
|
+
* `running` either way. The first response that shows CRIU is unavailable
|
|
57
|
+
* disables it for the rest of the process.
|
|
58
|
+
*
|
|
59
|
+
* **`find()` costs one request per match.** Docker's container LIST omits
|
|
60
|
+
* `State.StartedAt`, which is the only field distinguishing an interrupted
|
|
61
|
+
* creation (`startedAt: null` — holds storage forever, will never run) from an
|
|
62
|
+
* ordinary stopped sandbox, so each match is then inspected. Narrow with
|
|
63
|
+
* `labels`; an unfiltered query on a busy host inspects every container.
|
|
64
|
+
*
|
|
65
|
+
* **`capacity()` measures THIS host, so it reports nothing for a TCP daemon.**
|
|
66
|
+
* Over a `tcp://` endpoint the daemon writes to a different machine's storage,
|
|
67
|
+
* and returning this machine's numbers would be a confident wrong answer — so it
|
|
68
|
+
* returns empty `headroom` and says why. On a local socket it reports free bytes
|
|
69
|
+
* and free inodes on the daemon's `DockerRootDir` plus `MemAvailable`. Inodes are
|
|
70
|
+
* omitted (not reported as zero) on filesystems with dynamic inode allocation.
|
|
71
|
+
* `admits` is always `null`: Docker has no quota of its own to consult, so the
|
|
72
|
+
* floors and the refusal stay with the caller.
|
|
30
73
|
*
|
|
31
74
|
* @module
|
|
32
75
|
*/
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading back what already exists — descriptors, queries, volumes, and live
|
|
3
|
+
* resource changes.
|
|
4
|
+
*
|
|
5
|
+
* These are the operations a control plane needs when it is NOT using a sandbox:
|
|
6
|
+
* reconciling its own records against the daemon, finding what leaked, applying
|
|
7
|
+
* an entitlement to a sandbox it just adopted. molecule.dev wrote all of them
|
|
8
|
+
* directly against the Docker socket precisely because the provider interface
|
|
9
|
+
* stopped at "make me a sandbox and let me run things in it".
|
|
10
|
+
*
|
|
11
|
+
* Two rules run through the whole module.
|
|
12
|
+
*
|
|
13
|
+
* **An empty list means "looked, found none".** Every enumeration here lets its
|
|
14
|
+
* failure propagate rather than degrading to `[]`. The caller of a sweep DELETES
|
|
15
|
+
* what it is told about, so "I could not enumerate" and "there is nothing" must
|
|
16
|
+
* never arrive as the same value.
|
|
17
|
+
*
|
|
18
|
+
* **When in doubt, claim the resource is in use.** Attachment and usage decide
|
|
19
|
+
* whether something gets deleted, so an unreadable answer resolves to the safe
|
|
20
|
+
* side, loudly, rather than to the convenient one.
|
|
21
|
+
*
|
|
22
|
+
* @module
|
|
23
|
+
*/
|
|
24
|
+
import type { ListVolumesOptions, SandboxDescriptor, SandboxQuery, SandboxResources, VolumeInfo } from '@molecule/api-code-sandbox';
|
|
25
|
+
import type { DockerRequest } from './request.js';
|
|
26
|
+
/** What the inspection capabilities need from the provider. */
|
|
27
|
+
export interface InspectContext {
|
|
28
|
+
request: DockerRequest;
|
|
29
|
+
/** Label namespace the provider stamps on the sandboxes it creates. */
|
|
30
|
+
labelPrefix: string;
|
|
31
|
+
warn?: (message: string, meta?: Record<string, unknown>) => void;
|
|
32
|
+
debug?: (message: string, meta?: Record<string, unknown>) => void;
|
|
33
|
+
}
|
|
34
|
+
/** Shape of the fields this module reads from `GET /containers/{id}/json`. */
|
|
35
|
+
interface ContainerInspect {
|
|
36
|
+
Id: string;
|
|
37
|
+
Created?: string;
|
|
38
|
+
Image?: string;
|
|
39
|
+
State?: {
|
|
40
|
+
Running?: boolean;
|
|
41
|
+
StartedAt?: string;
|
|
42
|
+
};
|
|
43
|
+
Config?: {
|
|
44
|
+
Labels?: Record<string, string>;
|
|
45
|
+
Image?: string;
|
|
46
|
+
};
|
|
47
|
+
Mounts?: Array<{
|
|
48
|
+
Type?: string;
|
|
49
|
+
Name?: string;
|
|
50
|
+
}>;
|
|
51
|
+
NetworkSettings?: {
|
|
52
|
+
Ports?: Record<string, Array<{
|
|
53
|
+
HostIp?: string;
|
|
54
|
+
HostPort?: string;
|
|
55
|
+
}> | null> | null;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Turn a Docker inspect payload into a provider-agnostic descriptor.
|
|
60
|
+
*
|
|
61
|
+
* The status mapping is deliberately the SAME coarse one `get()` uses
|
|
62
|
+
* (running / stopped) rather than a finer one. Two views of the same sandbox
|
|
63
|
+
* that disagree about its state is a bug generator, and the case a finer mapping
|
|
64
|
+
* would have served — never-started wreckage — is carried honestly by
|
|
65
|
+
* `startedAt: null` instead.
|
|
66
|
+
*
|
|
67
|
+
* @param ctx - Inspection context.
|
|
68
|
+
* @param info - The daemon's inspect payload.
|
|
69
|
+
* @returns The descriptor.
|
|
70
|
+
*/
|
|
71
|
+
export declare function toDescriptor(ctx: InspectContext, info: ContainerInspect): SandboxDescriptor;
|
|
72
|
+
/**
|
|
73
|
+
* Describe one sandbox without building a handle for it.
|
|
74
|
+
*
|
|
75
|
+
* @param ctx - Inspection context.
|
|
76
|
+
* @param id - The sandbox id.
|
|
77
|
+
* @returns The descriptor, or `null` when no such sandbox exists.
|
|
78
|
+
*/
|
|
79
|
+
export declare function describeContainer(ctx: InspectContext, id: string): Promise<SandboxDescriptor | null>;
|
|
80
|
+
/**
|
|
81
|
+
* Find sandboxes by the labels the caller put on them.
|
|
82
|
+
*
|
|
83
|
+
* Two passes on purpose. The list endpoint answers cheaply but omits
|
|
84
|
+
* `State.StartedAt`, which is the one field that distinguishes wreckage from a
|
|
85
|
+
* sandbox that is merely stopped — so each match is then inspected. The label
|
|
86
|
+
* filter is what keeps that bounded; an unfiltered query over a busy host is
|
|
87
|
+
* one request per container and the caller should narrow it.
|
|
88
|
+
*
|
|
89
|
+
* A container that disappears between the two passes is skipped, not failed:
|
|
90
|
+
* that is a real answer (it is gone), and reconciliation loops race removals
|
|
91
|
+
* constantly.
|
|
92
|
+
*
|
|
93
|
+
* @param ctx - Inspection context.
|
|
94
|
+
* @param query - Narrowing by label, status and project.
|
|
95
|
+
* @returns Descriptors for every match.
|
|
96
|
+
*/
|
|
97
|
+
export declare function findContainers(ctx: InspectContext, query?: SandboxQuery): Promise<SandboxDescriptor[]>;
|
|
98
|
+
/**
|
|
99
|
+
* Enumerate named volumes and whether anything still has them attached.
|
|
100
|
+
*
|
|
101
|
+
* Attachment comes from Docker's `dangling` filter, which means "not used by any
|
|
102
|
+
* container, including stopped ones" — exactly the question a reclaim sweep is
|
|
103
|
+
* asking. When that second query fails, every volume is reported as ATTACHED and
|
|
104
|
+
* a warning is logged: an unreadable answer must not authorize deletion, and a
|
|
105
|
+
* volume is the only copy of a user's work.
|
|
106
|
+
*
|
|
107
|
+
* @param ctx - Inspection context.
|
|
108
|
+
* @param options - Narrowing by name prefix and attachment.
|
|
109
|
+
* @returns Every matching volume.
|
|
110
|
+
*/
|
|
111
|
+
export declare function listDockerVolumes(ctx: InspectContext, options?: ListVolumesOptions): Promise<VolumeInfo[]>;
|
|
112
|
+
/**
|
|
113
|
+
* Change a running sandbox's resource ceilings.
|
|
114
|
+
*
|
|
115
|
+
* `Memory` and `MemorySwap` are always sent together, and swap is always twice
|
|
116
|
+
* RAM. Docker treats them as one setting — updating `Memory` alone leaves the old
|
|
117
|
+
* swap ceiling, and this call is the LAST writer on the adoption path, so a swap
|
|
118
|
+
* value that is not recomputed here silently strips the headroom the creation
|
|
119
|
+
* path granted. That is not hypothetical; it happened to every claimed sandbox.
|
|
120
|
+
*
|
|
121
|
+
* `diskMB` throws. A provider that cannot enforce a ceiling must say so, because
|
|
122
|
+
* the caller's alternative — believing the quota is applied — is worse than a
|
|
123
|
+
* visible failure.
|
|
124
|
+
*
|
|
125
|
+
* @param ctx - Inspection context.
|
|
126
|
+
* @param containerId - The sandbox to update.
|
|
127
|
+
* @param resources - The ceilings to change; omitted fields keep their value.
|
|
128
|
+
*/
|
|
129
|
+
export declare function updateContainerResources(ctx: InspectContext, containerId: string, resources: Partial<SandboxResources>): Promise<void>;
|
|
130
|
+
export {};
|
|
131
|
+
//# sourceMappingURL=inspect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inspect.d.ts","sourceRoot":"","sources":["../src/inspect.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAElB,iBAAiB,EACjB,YAAY,EACZ,gBAAgB,EAChB,UAAU,EACX,MAAM,4BAA4B,CAAA;AAEnC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAEjD,+DAA+D;AAC/D,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,aAAa,CAAA;IACtB,uEAAuE;IACvE,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;IAChE,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;CAClE;AASD,8EAA8E;AAC9E,UAAU,gBAAgB;IACxB,EAAE,EAAE,MAAM,CAAA;IACV,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACjD,MAAM,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC5D,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAChD,eAAe,CAAC,EAAE;QAChB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC;YAAE,MAAM,CAAC,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,GAAG,IAAI,CAAC,GAAG,IAAI,CAAA;KACpF,CAAA;CACF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,gBAAgB,GAAG,iBAAiB,CA2B3F;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,cAAc,EACnB,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAYnC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,cAAc,EACnB,KAAK,CAAC,EAAE,YAAY,GACnB,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAwB9B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,cAAc,EACnB,OAAO,CAAC,EAAE,kBAAkB,GAC3B,OAAO,CAAC,UAAU,EAAE,CAAC,CAkCvB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,wBAAwB,CAC5C,GAAG,EAAE,cAAc,EACnB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,OAAO,CAAC,gBAAgB,CAAC,GACnC,OAAO,CAAC,IAAI,CAAC,CAqBf"}
|
package/dist/inspect.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading back what already exists — descriptors, queries, volumes, and live
|
|
3
|
+
* resource changes.
|
|
4
|
+
*
|
|
5
|
+
* These are the operations a control plane needs when it is NOT using a sandbox:
|
|
6
|
+
* reconciling its own records against the daemon, finding what leaked, applying
|
|
7
|
+
* an entitlement to a sandbox it just adopted. molecule.dev wrote all of them
|
|
8
|
+
* directly against the Docker socket precisely because the provider interface
|
|
9
|
+
* stopped at "make me a sandbox and let me run things in it".
|
|
10
|
+
*
|
|
11
|
+
* Two rules run through the whole module.
|
|
12
|
+
*
|
|
13
|
+
* **An empty list means "looked, found none".** Every enumeration here lets its
|
|
14
|
+
* failure propagate rather than degrading to `[]`. The caller of a sweep DELETES
|
|
15
|
+
* what it is told about, so "I could not enumerate" and "there is nothing" must
|
|
16
|
+
* never arrive as the same value.
|
|
17
|
+
*
|
|
18
|
+
* **When in doubt, claim the resource is in use.** Attachment and usage decide
|
|
19
|
+
* whether something gets deleted, so an unreadable answer resolves to the safe
|
|
20
|
+
* side, loudly, rather than to the convenient one.
|
|
21
|
+
*
|
|
22
|
+
* @module
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Docker's zero time. `State.StartedAt` holds it for a container that has been
|
|
26
|
+
* created and never started — the shape of an interrupted creation, which owns
|
|
27
|
+
* storage forever and will never run.
|
|
28
|
+
*/
|
|
29
|
+
const NEVER_STARTED = '0001-01-01T00:00:00Z';
|
|
30
|
+
/**
|
|
31
|
+
* Turn a Docker inspect payload into a provider-agnostic descriptor.
|
|
32
|
+
*
|
|
33
|
+
* The status mapping is deliberately the SAME coarse one `get()` uses
|
|
34
|
+
* (running / stopped) rather than a finer one. Two views of the same sandbox
|
|
35
|
+
* that disagree about its state is a bug generator, and the case a finer mapping
|
|
36
|
+
* would have served — never-started wreckage — is carried honestly by
|
|
37
|
+
* `startedAt: null` instead.
|
|
38
|
+
*
|
|
39
|
+
* @param ctx - Inspection context.
|
|
40
|
+
* @param info - The daemon's inspect payload.
|
|
41
|
+
* @returns The descriptor.
|
|
42
|
+
*/
|
|
43
|
+
export function toDescriptor(ctx, info) {
|
|
44
|
+
const labels = info.Config?.Labels ?? {};
|
|
45
|
+
const startedAt = info.State?.StartedAt;
|
|
46
|
+
const volumeFromMount = (info.Mounts ?? []).find((mount) => mount.Type === 'volume')?.Name;
|
|
47
|
+
const ports = [];
|
|
48
|
+
for (const [spec, bindings] of Object.entries(info.NetworkSettings?.Ports ?? {})) {
|
|
49
|
+
const sandboxPort = Number.parseInt(spec.split('/')[0] ?? '', 10);
|
|
50
|
+
if (!Number.isInteger(sandboxPort))
|
|
51
|
+
continue;
|
|
52
|
+
for (const binding of bindings ?? []) {
|
|
53
|
+
const port = Number.parseInt(binding.HostPort ?? '', 10);
|
|
54
|
+
if (!Number.isInteger(port) || port <= 0)
|
|
55
|
+
continue;
|
|
56
|
+
ports.push({ sandboxPort, host: binding.HostIp || '127.0.0.1', port });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
id: info.Id,
|
|
61
|
+
projectId: labels[`${ctx.labelPrefix}.projectId`] ?? null,
|
|
62
|
+
status: info.State?.Running ? 'running' : 'stopped',
|
|
63
|
+
labels,
|
|
64
|
+
createdAt: info.Created ?? null,
|
|
65
|
+
startedAt: !startedAt || startedAt === NEVER_STARTED ? null : startedAt,
|
|
66
|
+
templateRef: info.Config?.Image ?? info.Image ?? null,
|
|
67
|
+
volumeName: labels[`${ctx.labelPrefix}.volumeName`] ?? volumeFromMount ?? null,
|
|
68
|
+
ports,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Describe one sandbox without building a handle for it.
|
|
73
|
+
*
|
|
74
|
+
* @param ctx - Inspection context.
|
|
75
|
+
* @param id - The sandbox id.
|
|
76
|
+
* @returns The descriptor, or `null` when no such sandbox exists.
|
|
77
|
+
*/
|
|
78
|
+
export async function describeContainer(ctx, id) {
|
|
79
|
+
try {
|
|
80
|
+
const info = (await ctx.request(`/containers/${id}/json`));
|
|
81
|
+
return toDescriptor(ctx, info);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
85
|
+
// 404 is the answer "no such sandbox". Every other failure is a failure to
|
|
86
|
+
// look, and a caller that reads it as absence will happily "clean up" a
|
|
87
|
+
// sandbox that is alive and busy.
|
|
88
|
+
if (/\b404\b|no such container/i.test(message))
|
|
89
|
+
return null;
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Find sandboxes by the labels the caller put on them.
|
|
95
|
+
*
|
|
96
|
+
* Two passes on purpose. The list endpoint answers cheaply but omits
|
|
97
|
+
* `State.StartedAt`, which is the one field that distinguishes wreckage from a
|
|
98
|
+
* sandbox that is merely stopped — so each match is then inspected. The label
|
|
99
|
+
* filter is what keeps that bounded; an unfiltered query over a busy host is
|
|
100
|
+
* one request per container and the caller should narrow it.
|
|
101
|
+
*
|
|
102
|
+
* A container that disappears between the two passes is skipped, not failed:
|
|
103
|
+
* that is a real answer (it is gone), and reconciliation loops race removals
|
|
104
|
+
* constantly.
|
|
105
|
+
*
|
|
106
|
+
* @param ctx - Inspection context.
|
|
107
|
+
* @param query - Narrowing by label, status and project.
|
|
108
|
+
* @returns Descriptors for every match.
|
|
109
|
+
*/
|
|
110
|
+
export async function findContainers(ctx, query) {
|
|
111
|
+
const labelFilters = Object.entries(query?.labels ?? {}).map(([key, value]) => `${key}=${value}`);
|
|
112
|
+
if (query?.projectId)
|
|
113
|
+
labelFilters.push(`${ctx.labelPrefix}.projectId=${query.projectId}`);
|
|
114
|
+
const path = labelFilters.length > 0
|
|
115
|
+
? `/containers/json?all=true&filters=${encodeURIComponent(JSON.stringify({ label: labelFilters }))}`
|
|
116
|
+
: '/containers/json?all=true';
|
|
117
|
+
// Not wrapped: a failed listing must reach the caller. A recovery loop that
|
|
118
|
+
// reads "nothing to recover" from a broken query silently stops recovering.
|
|
119
|
+
const listed = (await ctx.request(path));
|
|
120
|
+
const descriptors = [];
|
|
121
|
+
for (const entry of Array.isArray(listed) ? listed : []) {
|
|
122
|
+
const descriptor = await describeContainer(ctx, entry.Id);
|
|
123
|
+
if (!descriptor) {
|
|
124
|
+
ctx.debug?.('Sandbox vanished between listing and inspection', { id: entry.Id });
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (query?.statuses && !query.statuses.includes(descriptor.status))
|
|
128
|
+
continue;
|
|
129
|
+
descriptors.push(descriptor);
|
|
130
|
+
}
|
|
131
|
+
return descriptors;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Enumerate named volumes and whether anything still has them attached.
|
|
135
|
+
*
|
|
136
|
+
* Attachment comes from Docker's `dangling` filter, which means "not used by any
|
|
137
|
+
* container, including stopped ones" — exactly the question a reclaim sweep is
|
|
138
|
+
* asking. When that second query fails, every volume is reported as ATTACHED and
|
|
139
|
+
* a warning is logged: an unreadable answer must not authorize deletion, and a
|
|
140
|
+
* volume is the only copy of a user's work.
|
|
141
|
+
*
|
|
142
|
+
* @param ctx - Inspection context.
|
|
143
|
+
* @param options - Narrowing by name prefix and attachment.
|
|
144
|
+
* @returns Every matching volume.
|
|
145
|
+
*/
|
|
146
|
+
export async function listDockerVolumes(ctx, options) {
|
|
147
|
+
const listed = (await ctx.request('/volumes'));
|
|
148
|
+
let unattached = null;
|
|
149
|
+
try {
|
|
150
|
+
const dangling = (await ctx.request(`/volumes?filters=${encodeURIComponent(JSON.stringify({ dangling: ['true'] }))}`));
|
|
151
|
+
unattached = new Set((dangling.Volumes ?? []).map((volume) => volume.Name));
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
ctx.warn?.('Could not determine volume attachment — reporting every volume as attached so nothing is reclaimed on a guess', { error });
|
|
155
|
+
}
|
|
156
|
+
const volumes = [];
|
|
157
|
+
for (const volume of listed.Volumes ?? []) {
|
|
158
|
+
if (options?.namePrefix && !volume.Name.startsWith(options.namePrefix))
|
|
159
|
+
continue;
|
|
160
|
+
const attached = unattached === null ? true : !unattached.has(volume.Name);
|
|
161
|
+
if (options?.attached !== undefined && options.attached !== attached)
|
|
162
|
+
continue;
|
|
163
|
+
const size = volume.UsageData?.Size;
|
|
164
|
+
volumes.push({
|
|
165
|
+
name: volume.Name,
|
|
166
|
+
attached,
|
|
167
|
+
createdAt: volume.CreatedAt ?? null,
|
|
168
|
+
// Docker reports -1 when it has not computed usage (that needs a
|
|
169
|
+
// system-wide scan), which is "unknown", not "empty".
|
|
170
|
+
sizeBytes: typeof size === 'number' && size >= 0 ? size : null,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return volumes;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Change a running sandbox's resource ceilings.
|
|
177
|
+
*
|
|
178
|
+
* `Memory` and `MemorySwap` are always sent together, and swap is always twice
|
|
179
|
+
* RAM. Docker treats them as one setting — updating `Memory` alone leaves the old
|
|
180
|
+
* swap ceiling, and this call is the LAST writer on the adoption path, so a swap
|
|
181
|
+
* value that is not recomputed here silently strips the headroom the creation
|
|
182
|
+
* path granted. That is not hypothetical; it happened to every claimed sandbox.
|
|
183
|
+
*
|
|
184
|
+
* `diskMB` throws. A provider that cannot enforce a ceiling must say so, because
|
|
185
|
+
* the caller's alternative — believing the quota is applied — is worse than a
|
|
186
|
+
* visible failure.
|
|
187
|
+
*
|
|
188
|
+
* @param ctx - Inspection context.
|
|
189
|
+
* @param containerId - The sandbox to update.
|
|
190
|
+
* @param resources - The ceilings to change; omitted fields keep their value.
|
|
191
|
+
*/
|
|
192
|
+
export async function updateContainerResources(ctx, containerId, resources) {
|
|
193
|
+
if (resources.diskMB !== undefined) {
|
|
194
|
+
throw new Error('The Docker sandbox provider cannot cap a container or volume size portably ' +
|
|
195
|
+
'(it needs a specific storage driver, e.g. overlay2 on xfs with pquota, and ' +
|
|
196
|
+
'errors on the common overlay2/ext4 host), so `diskMB` cannot be applied. ' +
|
|
197
|
+
'Cap disk at the host or volume level instead.');
|
|
198
|
+
}
|
|
199
|
+
const update = {};
|
|
200
|
+
if (resources.cpu !== undefined)
|
|
201
|
+
update.NanoCPUs = Math.round(resources.cpu * 1e9);
|
|
202
|
+
if (resources.memoryMB !== undefined) {
|
|
203
|
+
const memoryBytes = Math.round(resources.memoryMB * 1024 * 1024);
|
|
204
|
+
update.Memory = memoryBytes;
|
|
205
|
+
update.MemorySwap = memoryBytes * 2;
|
|
206
|
+
}
|
|
207
|
+
if (resources.pidsLimit !== undefined)
|
|
208
|
+
update.PidsLimit = resources.pidsLimit;
|
|
209
|
+
if (Object.keys(update).length === 0)
|
|
210
|
+
return;
|
|
211
|
+
await ctx.request(`/containers/${containerId}/update`, 'POST', update);
|
|
212
|
+
}
|
package/dist/provider.d.ts
CHANGED
|
@@ -8,6 +8,20 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { SandboxProvider } from '@molecule/api-code-sandbox';
|
|
10
10
|
import type { DockerConfig } from './types.js';
|
|
11
|
+
/**
|
|
12
|
+
* Validate the ports a caller asked to publish, falling back to this provider's
|
|
13
|
+
* historical API + Vite pair when it named none.
|
|
14
|
+
*
|
|
15
|
+
* Rejects rather than filters: a caller that asked for a port it cannot have
|
|
16
|
+
* needs to hear so, because the alternative is a dev server running inside a
|
|
17
|
+
* sandbox that nothing on the outside can ever reach — which looks like a broken
|
|
18
|
+
* application, not a bad argument.
|
|
19
|
+
*
|
|
20
|
+
* @param ports - Ports inside the sandbox the caller wants reachable.
|
|
21
|
+
* @returns The ports to expose and bind.
|
|
22
|
+
* @throws {Error} When a port is not a valid TCP port number.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolvePublishPorts(ports?: number[]): number[];
|
|
11
25
|
/**
|
|
12
26
|
* Retry a Docker API operation that failed with a TRANSIENT fault — a request
|
|
13
27
|
* timeout or a connection-level reset, i.e. the daemon was momentarily overwhelmed,
|
package/dist/provider.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAQH,OAAO,KAAK,EAcV,eAAe,EAOhB,MAAM,4BAA4B,CAAA;AAoBnC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AA6J9C;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAQ9D;AAkCD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,kBAAkB,CAAC,CAAC,EACxC,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,IAAI,EAAE;IACJ,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAA;IACjC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAA;IACrC,GAAG,CAAC,EAAE;QAAE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;KAAE,CAAA;CAC1D,GACA,OAAO,CAAC,CAAC,CAAC,CA2BZ;AA21CD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,MAAM,CAAC,EAAE,YAAY,GAAG,eAAe,CAErE"}
|