@exagone313/dsh-podman 0.2.0-rc.3 → 0.2.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 +14 -2
- package/README.zh.md +14 -2
- package/cordis.patch.yml +58 -0
- package/dist/approval-reasons.d.ts +3 -4
- package/dist/approval-reasons.js +40 -49
- package/dist/approval-reasons.test.js +43 -15
- package/dist/approval.js +27 -10
- package/dist/approval.test.js +165 -34
- package/dist/card-route.d.ts +3 -2
- package/dist/card-route.js +154 -42
- package/dist/card-route.test.js +85 -43
- package/dist/card-test-support.js +39 -66
- package/dist/client/ContainerCard.d.ts +1 -1
- package/dist/client/ContainerCard.js +92 -50
- package/dist/client/card-protocol.d.ts +6 -3
- package/dist/client/container-card-caches.js +11 -6
- package/dist/client/container-card-controller.d.ts +16 -18
- package/dist/client/container-card-controller.js +92 -40
- package/dist/client/container-card-create-modal.d.ts +2 -2
- package/dist/client/container-card-create-modal.js +13 -5
- package/dist/client/container-card-default-env.d.ts +10 -0
- package/dist/client/container-card-default-env.js +69 -0
- package/dist/client/container-card-directory.d.ts +2 -2
- package/dist/client/container-card-directory.js +12 -4
- package/dist/client/container-card-editors.d.ts +3 -3
- package/dist/client/container-card-editors.js +104 -32
- package/dist/client/container-card-images.d.ts +4 -4
- package/dist/client/container-card-images.js +19 -6
- package/dist/client/container-card-paths.d.ts +3 -3
- package/dist/client/container-card-paths.js +18 -11
- package/dist/client/container-card-row.d.ts +10 -6
- package/dist/client/container-card-row.js +82 -13
- package/dist/client/container-card-secrets.d.ts +3 -3
- package/dist/client/container-card-secrets.js +8 -6
- package/dist/client/container-card-shared.d.ts +4 -17
- package/dist/client/container-card-shared.js +9 -25
- package/dist/client/container-card-styles.d.ts +2 -8
- package/dist/client/container-card-styles.js +14 -52
- package/dist/client/container-card-volumes.d.ts +2 -2
- package/dist/client/container-card-volumes.js +9 -5
- package/dist/client/container-card-workspace.d.ts +8 -6
- package/dist/client/container-card-workspace.js +9 -3
- package/dist/client/directory-picker.js +2 -1
- package/dist/client/index.js +13141 -2036
- package/dist/client/locales.d.ts +6 -1
- package/dist/client/locales.js +106 -38
- package/dist/client/podman-terminal-guide.d.ts +11 -0
- package/dist/client/podman-terminal-guide.js +208 -0
- package/dist/client/podman-terminal-title.d.ts +7 -0
- package/dist/client/podman-terminal-title.js +30 -0
- package/dist/client/podman-terminal.d.ts +26 -0
- package/dist/client/podman-terminal.js +412 -0
- package/dist/client/read-only-approval.d.ts +2 -2
- package/dist/client/read-only-approval.js +13 -3
- package/dist/client/slot-contract.d.ts +0 -11
- package/dist/client/terminal-preference.d.ts +26 -0
- package/dist/client/terminal-preference.js +67 -0
- package/dist/client/terminal-protocol.d.ts +85 -0
- package/dist/client/terminal-protocol.js +14 -0
- package/dist/client/terminal-tab.d.ts +60 -0
- package/dist/client/terminal-tab.js +35 -0
- package/dist/client/terminal-targets.d.ts +33 -0
- package/dist/client/terminal-targets.js +65 -0
- package/dist/client/terminal-titles.d.ts +23 -0
- package/dist/client/terminal-titles.js +52 -0
- package/dist/client/terminal-transport.d.ts +63 -0
- package/dist/client/terminal-transport.js +207 -0
- package/dist/client/tool-views.js +215 -49
- package/dist/container-env.d.ts +11 -0
- package/dist/container-env.js +96 -0
- package/dist/container-env.test.d.ts +1 -0
- package/dist/container-env.test.js +93 -0
- package/dist/containers.test.js +196 -18
- package/dist/daemons.test.js +18 -4
- package/dist/dsh-version.d.ts +1 -0
- package/dist/dsh-version.js +46 -0
- package/dist/env-rows.d.ts +14 -0
- package/dist/env-rows.js +52 -0
- package/dist/env-rows.test.d.ts +1 -0
- package/dist/env-rows.test.js +43 -0
- package/dist/fs-provider.d.ts +1 -0
- package/dist/fs-provider.js +26 -12
- package/dist/fs-provider.test.js +51 -10
- package/dist/generated/version.d.ts +2 -2
- package/dist/generated/version.js +2 -2
- package/dist/grpc/proto/dshctl/v1/control.proto +11 -1
- package/dist/grpc/proto/dshguest/v1/guest.proto +5 -0
- package/dist/grpc/runtime-client.d.ts +3 -1
- package/dist/grpc/runtime-client.js +90 -17
- package/dist/guest-rpc.d.ts +15 -19
- package/dist/guest-rpc.js +289 -70
- package/dist/guest-rpc.test.d.ts +1 -0
- package/dist/guest-rpc.test.js +241 -0
- package/dist/guest-terminal.d.ts +30 -0
- package/dist/guest-terminal.js +196 -0
- package/dist/images.test.js +10 -3
- package/dist/index.d.ts +11 -16
- package/dist/index.js +42 -36
- package/dist/locales.test.d.ts +1 -0
- package/dist/locales.test.js +34 -0
- package/dist/misc.test.js +38 -10
- package/dist/mount-enums.js +2 -1
- package/dist/mount-input.d.ts +2 -0
- package/dist/mount-input.js +38 -22
- package/dist/mounts.test.js +81 -11
- package/dist/naming.test.d.ts +1 -0
- package/dist/naming.test.js +28 -0
- package/dist/output-reader.js +36 -4
- package/dist/package-deps.test.d.ts +1 -0
- package/dist/package-deps.test.js +49 -0
- package/dist/paths.test.js +4 -2
- package/dist/plugin-meta.test.d.ts +1 -0
- package/dist/plugin-meta.test.js +29 -0
- package/dist/project-path.js +33 -7
- package/dist/project-path.test.js +14 -0
- package/dist/prompts.d.ts +0 -4
- package/dist/prompts.js +15 -84
- package/dist/read-only-shell.js +3 -1
- package/dist/read-only-shell.test.js +80 -25
- package/dist/runtime-client.test.d.ts +1 -0
- package/dist/runtime-client.test.js +53 -0
- package/dist/secrets.test.js +21 -6
- package/dist/settings-commands.test.js +61 -14
- package/dist/settings-create.test.js +32 -10
- package/dist/settings-default-env.test.d.ts +1 -0
- package/dist/settings-default-env.test.js +175 -0
- package/dist/settings-mounts.test.js +46 -7
- package/dist/settings-schema.d.ts +6 -11
- package/dist/settings-schema.js +4 -8
- package/dist/settings-schema.test.d.ts +1 -0
- package/dist/settings-schema.test.js +35 -0
- package/dist/settings-workspaces.test.js +38 -63
- package/dist/spill-store.d.ts +1 -0
- package/dist/spill-store.js +17 -4
- package/dist/spill-store.test.js +31 -8
- package/dist/subprocess.d.ts +4 -0
- package/dist/subprocess.js +148 -162
- package/dist/subprocess.test.js +118 -8
- package/dist/terminal-preference.test.d.ts +1 -0
- package/dist/terminal-preference.test.js +62 -0
- package/dist/terminal-route.d.ts +10 -0
- package/dist/terminal-route.js +270 -0
- package/dist/terminal-route.test.d.ts +1 -0
- package/dist/terminal-route.test.js +265 -0
- package/dist/terminal-sessions.d.ts +69 -0
- package/dist/terminal-sessions.js +252 -0
- package/dist/terminal-shells.d.ts +22 -0
- package/dist/terminal-shells.js +99 -0
- package/dist/terminal-tab.test.d.ts +1 -0
- package/dist/terminal-tab.test.js +58 -0
- package/dist/terminal-targets.test.d.ts +1 -0
- package/dist/terminal-targets.test.js +43 -0
- package/dist/terminal-titles.test.d.ts +1 -0
- package/dist/terminal-titles.test.js +36 -0
- package/dist/test-support.d.ts +35 -5
- package/dist/test-support.js +91 -29
- package/dist/tool-defs.js +19 -4
- package/dist/tool-handlers.js +80 -28
- package/dist/tool-params.js +19 -5
- package/dist/version-compat.d.ts +4 -0
- package/dist/version-compat.js +29 -0
- package/dist/version-compat.test.d.ts +1 -0
- package/dist/version-compat.test.js +27 -0
- package/dist/volumes.test.js +1 -1
- package/dist/workspace-binding.d.ts +19 -10
- package/dist/workspace-binding.js +252 -78
- package/dist/workspace-binding.test.js +202 -27
- package/docs/architecture.md +71 -20
- package/docs/architecture.zh.md +62 -12
- package/docs/configuration.md +61 -20
- package/docs/configuration.zh.md +53 -19
- package/docs/development-prompt.md +72 -0
- package/docs/development-prompt.zh.md +69 -0
- package/docs/development.md +184 -29
- package/docs/development.zh.md +165 -26
- package/docs/install-dsh-and-dsh-podman.md +24 -21
- package/docs/install-dsh-and-dsh-podman.zh.md +20 -21
- package/docs/uninstall.md +83 -0
- package/docs/uninstall.zh.md +79 -0
- package/docs/update.md +120 -0
- package/docs/update.zh.md +114 -0
- package/docs/usage.md +108 -61
- package/docs/usage.zh.md +76 -38
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +58 -25
- package/quadlet/dsh-podman-orchestrator.container +46 -0
- package/quadlet/dsh.container +35 -0
- package/LICENSE.pkg +0 -17103
- package/dist/preferences.d.ts +0 -6
- package/dist/preferences.js +0 -27
package/dist/card-route.js
CHANGED
|
@@ -6,11 +6,26 @@
|
|
|
6
6
|
// request, so neither the snapshot nor the commands touch the persisted
|
|
7
7
|
// settings document.
|
|
8
8
|
import { CARD_PATH, } from "./client/card-protocol.js";
|
|
9
|
-
import { renderCacheCleanNotice, resolveReasonLocale } from "./approval-reasons.js";
|
|
10
|
-
import {
|
|
9
|
+
import { renderCacheCleanNotice, renderDefaultEnvSyncNotice, resolveReasonLocale, } from "./approval-reasons.js";
|
|
10
|
+
import { mergeDefaultEnv, missingDefaultEnv } from "./container-env.js";
|
|
11
|
+
import { defaultMountMode, mountKindToProto, mountModeToProto, } from "./mount-enums.js";
|
|
12
|
+
import { mountInputToProto, validateMountInput } from "./mount-input.js";
|
|
11
13
|
import { unaryGuest } from "./guest-rpc.js";
|
|
12
14
|
import { workspaceSlug } from "./workspace-binding.js";
|
|
13
15
|
import { GIT_COMMIT, VERSION } from "./generated/version.js";
|
|
16
|
+
import { versionState as pluginVersionState } from "./version-compat.js";
|
|
17
|
+
import { dshVersion } from "./dsh-version.js";
|
|
18
|
+
// readOrchestratorVersion asks the orchestrator for its version, or returns ""
|
|
19
|
+
// when the call fails (an orchestrator predating the handshake).
|
|
20
|
+
async function readOrchestratorVersion(resolver) {
|
|
21
|
+
try {
|
|
22
|
+
const response = await resolver.control("getVersion", {});
|
|
23
|
+
return String(response?.version ?? "");
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return "";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
14
29
|
// The cache clean modes the settings card can request, mapped to the control
|
|
15
30
|
// plane's enum names.
|
|
16
31
|
const CACHE_CLEAN_MODES = {
|
|
@@ -25,17 +40,32 @@ function cacheCleanModeToProto(mode) {
|
|
|
25
40
|
throw new Error(`unknown cache clean mode: ${mode}`);
|
|
26
41
|
return proto;
|
|
27
42
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
// containerMountView and imageView rebuild the snapshot's nested objects from
|
|
44
|
+
// an explicit allow-list, so a field added to the proto later cannot reach the
|
|
45
|
+
// browser just because it exists on the wire (see AGENTS.md "Security").
|
|
46
|
+
function containerMountView(mount) {
|
|
47
|
+
return {
|
|
48
|
+
projectName: stringField(mount?.projectName),
|
|
49
|
+
destination: stringField(mount?.destination),
|
|
50
|
+
kind: stringField(mount?.kind),
|
|
51
|
+
mode: stringField(mount?.mode),
|
|
52
|
+
volume: stringField(mount?.volume),
|
|
53
|
+
secret: stringField(mount?.secret),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function imageView(image) {
|
|
57
|
+
return {
|
|
58
|
+
imageId: stringField(image?.imageId),
|
|
59
|
+
parent: stringField(image?.parent),
|
|
60
|
+
packages: Array.isArray(image?.packages) ? image.packages.map(String) : [],
|
|
61
|
+
imageTag: stringField(image?.imageTag),
|
|
62
|
+
builtAt: stringField(image?.builtAt),
|
|
63
|
+
isBase: Boolean(image?.isBase),
|
|
64
|
+
status: stringField(image?.status),
|
|
65
|
+
primitive: stringField(image?.primitive),
|
|
66
|
+
packageManager: stringField(image?.packageManager),
|
|
67
|
+
basePublic: Boolean(image?.basePublic),
|
|
68
|
+
};
|
|
39
69
|
}
|
|
40
70
|
function orchestratorWorkspaceViews(raw) {
|
|
41
71
|
return (raw ?? []).map((workspace) => ({
|
|
@@ -63,12 +93,15 @@ function dshWorkspaceViews(registry, projectsRoot) {
|
|
|
63
93
|
: path;
|
|
64
94
|
return {
|
|
65
95
|
workspaceSlug: workspaceSlug(workspace.id),
|
|
66
|
-
projectName: projectName || String(workspace.title ?? "") ||
|
|
96
|
+
projectName: projectName || String(workspace.title ?? "") ||
|
|
97
|
+
String(workspace.id ?? ""),
|
|
67
98
|
containerName: "",
|
|
68
99
|
imageId: "",
|
|
69
100
|
status: "",
|
|
70
101
|
createdAt: workspace.createdAt ?? "",
|
|
71
|
-
mounts: projectName !== ""
|
|
102
|
+
mounts: projectName !== ""
|
|
103
|
+
? [{ projectName, mode: "MOUNT_MODE_READ_WRITE" }]
|
|
104
|
+
: [],
|
|
72
105
|
};
|
|
73
106
|
});
|
|
74
107
|
}
|
|
@@ -99,17 +132,49 @@ function dshWorkspaceCwd(registry, slug) {
|
|
|
99
132
|
}
|
|
100
133
|
/** Build the live snapshot the card renders. */
|
|
101
134
|
export async function cardSnapshot(resolver, workspaceRegistry) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
135
|
+
// The handshake RPC is exempt from the orchestrator's version check, so it
|
|
136
|
+
// succeeds even when the rest is refused; an older orchestrator that has no
|
|
137
|
+
// GetVersion yet leaves the version unknown.
|
|
138
|
+
const orchestratorVersion = await readOrchestratorVersion(resolver);
|
|
139
|
+
let listings;
|
|
140
|
+
try {
|
|
141
|
+
listings = await Promise.all([
|
|
142
|
+
resolver.control("listContainers", {}),
|
|
143
|
+
resolver.control("listImages", {}),
|
|
144
|
+
resolver.control("listWorkspaces", {}),
|
|
145
|
+
resolver.control("listVolumes", {}),
|
|
146
|
+
resolver.control("listSecrets", {}),
|
|
147
|
+
resolver.control("listCaches", {}),
|
|
148
|
+
]);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
// An incompatible plugin is refused before any handler runs, so the card
|
|
152
|
+
// reports the mismatch instead of an unexplained empty snapshot.
|
|
153
|
+
if (error?.code !== "FailedPrecondition") {
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
version: VERSION,
|
|
158
|
+
commit: GIT_COMMIT,
|
|
159
|
+
dshVersion: dshVersion(),
|
|
160
|
+
orchestratorVersion,
|
|
161
|
+
versionState: "major-mismatch",
|
|
162
|
+
projectsRoot: resolver.getConfig().projectsRoot,
|
|
163
|
+
workspaces: [],
|
|
164
|
+
containers: [],
|
|
165
|
+
images: [],
|
|
166
|
+
volumes: [],
|
|
167
|
+
secrets: [],
|
|
168
|
+
caches: [],
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const [containers, images, workspaces, volumes, secrets, caches] = listings;
|
|
110
172
|
return {
|
|
111
173
|
version: VERSION,
|
|
112
174
|
commit: GIT_COMMIT,
|
|
175
|
+
dshVersion: dshVersion(),
|
|
176
|
+
orchestratorVersion,
|
|
177
|
+
versionState: pluginVersionState(VERSION, orchestratorVersion),
|
|
113
178
|
projectsRoot: resolver.getConfig().projectsRoot,
|
|
114
179
|
workspaces: mergeWorkspaceViews(dshWorkspaceViews(workspaceRegistry, resolver.getConfig().projectsRoot), orchestratorWorkspaceViews(workspaces.workspaces)),
|
|
115
180
|
containers: (containers.containers ?? []).map((container) => ({
|
|
@@ -118,12 +183,12 @@ export async function cardSnapshot(resolver, workspaceRegistry) {
|
|
|
118
183
|
imageId: container.imageId ?? "",
|
|
119
184
|
status: container.status ?? "",
|
|
120
185
|
createdAt: container.createdAt ?? "",
|
|
121
|
-
mounts: container.mounts ?? [],
|
|
186
|
+
mounts: (container.mounts ?? []).map(containerMountView),
|
|
122
187
|
paths: container.paths ?? [],
|
|
123
188
|
env: container.env ?? {},
|
|
124
189
|
secretEnv: container.secretEnv ?? {},
|
|
125
190
|
})),
|
|
126
|
-
images: images.images ?? [],
|
|
191
|
+
images: (images.images ?? []).map(imageView),
|
|
127
192
|
volumes: (volumes.volumes ?? []).map((volume) => ({
|
|
128
193
|
name: volume.name ?? "",
|
|
129
194
|
})),
|
|
@@ -143,7 +208,9 @@ export async function cardSnapshot(resolver, workspaceRegistry) {
|
|
|
143
208
|
* Run one card command, returning the notice to show (usually empty). `ctx` is
|
|
144
209
|
* the settings-injected context, read only for the session locale.
|
|
145
210
|
*/
|
|
146
|
-
export async function runCommand(
|
|
211
|
+
export async function runCommand(_ctx, resolver, workspaceRegistry, raw,
|
|
212
|
+
// The live UI locale, read per notice; absent in tests, which render English.
|
|
213
|
+
readLocale) {
|
|
147
214
|
const command = normalizeCommand(raw);
|
|
148
215
|
let notice = "";
|
|
149
216
|
switch (command.op) {
|
|
@@ -152,13 +219,18 @@ export async function runCommand(ctx, resolver, workspaceRegistry, raw) {
|
|
|
152
219
|
workspaceSlug: command.workspace,
|
|
153
220
|
imageId: command.image === "" ? undefined : command.image,
|
|
154
221
|
};
|
|
155
|
-
const
|
|
222
|
+
const projectsRoot = resolver.getConfig().projectsRoot;
|
|
223
|
+
const mounts = command.mounts.map((mount) => mountInputToProto(mount, projectsRoot));
|
|
156
224
|
if (mounts.length > 0)
|
|
157
225
|
payload.mounts = mounts;
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
226
|
+
// The card's create is a creation path: seed the default environment
|
|
227
|
+
// under whatever the modal collected. A later recreate is authoritative,
|
|
228
|
+
// which is how a seeded value is removed again.
|
|
229
|
+
const env = mergeDefaultEnv(resolver.getConfig().containerEnv, command.env);
|
|
230
|
+
if (Object.keys(env).length > 0)
|
|
231
|
+
payload.env = env;
|
|
232
|
+
if (Object.keys(command.secretEnv).length > 0) {
|
|
233
|
+
payload.secretEnv = command.secretEnv;
|
|
162
234
|
}
|
|
163
235
|
if (command.paths.length > 0)
|
|
164
236
|
payload.paths = [...command.paths];
|
|
@@ -180,6 +252,7 @@ export async function runCommand(ctx, resolver, workspaceRegistry, raw) {
|
|
|
180
252
|
const m = command.mount;
|
|
181
253
|
if (m === null)
|
|
182
254
|
break;
|
|
255
|
+
validateMountInput(m, resolver.getConfig().projectsRoot);
|
|
183
256
|
const kind = mountKindToProto(m.kind || undefined);
|
|
184
257
|
const mode = mountModeToProto(m.mode || defaultMountMode(m.kind || undefined));
|
|
185
258
|
const request = {
|
|
@@ -238,6 +311,9 @@ export async function runCommand(ctx, resolver, workspaceRegistry, raw) {
|
|
|
238
311
|
const m = command.mount;
|
|
239
312
|
if (m === null)
|
|
240
313
|
break;
|
|
314
|
+
// An update sets a mode, so a secret mount must not be made read-write
|
|
315
|
+
// here either.
|
|
316
|
+
validateMountInput(m, resolver.getConfig().projectsRoot);
|
|
241
317
|
const kind = mountKindToProto(m.kind || undefined);
|
|
242
318
|
const request = {
|
|
243
319
|
workspaceSlug: command.workspace,
|
|
@@ -277,6 +353,7 @@ export async function runCommand(ctx, resolver, workspaceRegistry, raw) {
|
|
|
277
353
|
case "remove":
|
|
278
354
|
await resolver.control("removeContainer", {
|
|
279
355
|
workspaceSlug: command.workspace,
|
|
356
|
+
...(command.container !== "" ? { container: command.container } : {}),
|
|
280
357
|
});
|
|
281
358
|
break;
|
|
282
359
|
case "workspace_remove":
|
|
@@ -287,6 +364,7 @@ export async function runCommand(ctx, resolver, workspaceRegistry, raw) {
|
|
|
287
364
|
case "recreate":
|
|
288
365
|
await resolver.control("recreateContainer", {
|
|
289
366
|
workspaceSlug: command.workspace,
|
|
367
|
+
...(command.container !== "" ? { container: command.container } : {}),
|
|
290
368
|
imageId: command.image === "" ? undefined : command.image,
|
|
291
369
|
...(Object.keys(command.env).length > 0 ? { env: command.env } : {}),
|
|
292
370
|
});
|
|
@@ -326,7 +404,7 @@ export async function runCommand(ctx, resolver, workspaceRegistry, raw) {
|
|
|
326
404
|
await resolver.control("addContainerSecret", {
|
|
327
405
|
workspaceSlug: command.workspace,
|
|
328
406
|
container: command.container || "default",
|
|
329
|
-
env: command.
|
|
407
|
+
env: command.secretEnvName,
|
|
330
408
|
secret: command.secret,
|
|
331
409
|
});
|
|
332
410
|
break;
|
|
@@ -334,7 +412,7 @@ export async function runCommand(ctx, resolver, workspaceRegistry, raw) {
|
|
|
334
412
|
await resolver.control("removeContainerSecret", {
|
|
335
413
|
workspaceSlug: command.workspace,
|
|
336
414
|
container: command.container || "default",
|
|
337
|
-
env: command.
|
|
415
|
+
env: command.secretEnvName,
|
|
338
416
|
});
|
|
339
417
|
break;
|
|
340
418
|
case "image_build":
|
|
@@ -350,11 +428,44 @@ export async function runCommand(ctx, resolver, workspaceRegistry, raw) {
|
|
|
350
428
|
case "image_base_pull":
|
|
351
429
|
await resolver.control("pullBaseImage", { name: command.workspace });
|
|
352
430
|
break;
|
|
431
|
+
case "default_env_sync": {
|
|
432
|
+
// Add the default environment to containers that already exist. Only the
|
|
433
|
+
// keys a container is missing are added, so an explicit value is never
|
|
434
|
+
// overwritten, and a stopped container is left for its next start (which
|
|
435
|
+
// seeds the defaults). A recreate applies the merged map.
|
|
436
|
+
const defaults = resolver.getConfig().containerEnv ?? {};
|
|
437
|
+
if (Object.keys(defaults).length === 0)
|
|
438
|
+
break;
|
|
439
|
+
const listings = await resolver.control("listContainers", {});
|
|
440
|
+
let applied = 0;
|
|
441
|
+
let skipped = 0;
|
|
442
|
+
for (const row of listings.containers ?? []) {
|
|
443
|
+
if (command.workspace !== "" && row.workspaceSlug !== command.workspace) {
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
const current = (row.env ?? {});
|
|
447
|
+
const missing = missingDefaultEnv(defaults, current);
|
|
448
|
+
if (Object.keys(missing).length === 0)
|
|
449
|
+
continue;
|
|
450
|
+
if (row.status !== "running") {
|
|
451
|
+
skipped++;
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
await resolver.control("recreateContainer", {
|
|
455
|
+
workspaceSlug: row.workspaceSlug,
|
|
456
|
+
container: row.containerName,
|
|
457
|
+
env: mergeDefaultEnv(defaults, current),
|
|
458
|
+
});
|
|
459
|
+
applied++;
|
|
460
|
+
}
|
|
461
|
+
notice = renderDefaultEnvSyncNotice(resolveReasonLocale(readLocale?.()), applied, skipped);
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
353
464
|
case "cache_clean": {
|
|
354
465
|
const result = (await resolver.control("cleanCaches", {
|
|
355
466
|
mode: cacheCleanModeToProto(command.cacheMode),
|
|
356
467
|
}));
|
|
357
|
-
notice = renderCacheCleanNotice(resolveReasonLocale(
|
|
468
|
+
notice = renderCacheCleanNotice(resolveReasonLocale(readLocale?.()), Number(result.removedFiles ?? 0));
|
|
358
469
|
break;
|
|
359
470
|
}
|
|
360
471
|
default:
|
|
@@ -372,8 +483,9 @@ function stringField(value, fallback = "") {
|
|
|
372
483
|
return typeof value === "string" ? value : fallback;
|
|
373
484
|
}
|
|
374
485
|
function stringMap(value) {
|
|
375
|
-
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
486
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
376
487
|
return {};
|
|
488
|
+
}
|
|
377
489
|
const result = {};
|
|
378
490
|
for (const [key, item] of Object.entries(value)) {
|
|
379
491
|
result[key] = String(item);
|
|
@@ -416,16 +528,16 @@ function normalizeCommand(raw) {
|
|
|
416
528
|
env: stringMap(raw.env),
|
|
417
529
|
container: stringField(raw.container),
|
|
418
530
|
secret: stringField(raw.secret),
|
|
419
|
-
|
|
531
|
+
secretEnvName: stringField(raw.secretEnvName),
|
|
420
532
|
length: typeof raw.length === "number" ? raw.length : 0,
|
|
421
533
|
charset: stringField(raw.charset),
|
|
422
534
|
packages: Array.isArray(raw.packages) ? raw.packages.map(String) : [],
|
|
423
|
-
|
|
535
|
+
secretEnv: stringMap(raw.secretEnv),
|
|
424
536
|
cacheMode: stringField(raw.cacheMode),
|
|
425
537
|
mount: mountField(raw.mount),
|
|
426
538
|
};
|
|
427
539
|
}
|
|
428
|
-
async function handleCardRequest(request, ctx, resolver, workspaceRegistry) {
|
|
540
|
+
async function handleCardRequest(request, ctx, resolver, workspaceRegistry, readLocale) {
|
|
429
541
|
if (request.method === "GET") {
|
|
430
542
|
try {
|
|
431
543
|
return jsonResponse(200, await cardSnapshot(resolver, workspaceRegistry));
|
|
@@ -451,7 +563,7 @@ async function handleCardRequest(request, ctx, resolver, workspaceRegistry) {
|
|
|
451
563
|
return jsonResponse(400, { error: "a command is required" });
|
|
452
564
|
}
|
|
453
565
|
try {
|
|
454
|
-
const notice = await runCommand(ctx, resolver, workspaceRegistry, command);
|
|
566
|
+
const notice = await runCommand(ctx, resolver, workspaceRegistry, command, readLocale);
|
|
455
567
|
const result = notice === "" ? {} : { notice };
|
|
456
568
|
return jsonResponse(200, result);
|
|
457
569
|
}
|
|
@@ -466,13 +578,13 @@ async function handleCardRequest(request, ctx, resolver, workspaceRegistry) {
|
|
|
466
578
|
* harness API path, so the carrier applies its Host/Origin fence and browser
|
|
467
579
|
* authentication before the handler runs.
|
|
468
580
|
*/
|
|
469
|
-
export function registerCardRoute(ctx, resolver, workspaceRegistry) {
|
|
470
|
-
ctx.inject(["connection"
|
|
581
|
+
export function registerCardRoute(ctx, resolver, workspaceRegistry, readLocale) {
|
|
582
|
+
ctx.inject(["connection"], (connectionCtx) => {
|
|
471
583
|
connectionCtx.connection.fetch.register({
|
|
472
584
|
path: CARD_PATH,
|
|
473
585
|
methods: ["GET", "POST"],
|
|
474
586
|
requestBody: "buffered",
|
|
475
|
-
fetch: (request) => handleCardRequest(request, connectionCtx, resolver, workspaceRegistry),
|
|
587
|
+
fetch: (request) => handleCardRequest(request, connectionCtx, resolver, workspaceRegistry, readLocale),
|
|
476
588
|
});
|
|
477
589
|
});
|
|
478
590
|
}
|
package/dist/card-route.test.js
CHANGED
|
@@ -5,7 +5,6 @@ import { test } from "node:test";
|
|
|
5
5
|
import { strict as assert } from "node:assert";
|
|
6
6
|
import { CARD_PATH } from "./client/card-protocol.js";
|
|
7
7
|
import { registerCardRoute } from "./card-route.js";
|
|
8
|
-
import { installContainerPreferences } from "./preferences.js";
|
|
9
8
|
function fakeResolver() {
|
|
10
9
|
const calls = [];
|
|
11
10
|
const resolver = {
|
|
@@ -17,8 +16,11 @@ function fakeResolver() {
|
|
|
17
16
|
setConfig: () => { },
|
|
18
17
|
async control(method, request) {
|
|
19
18
|
calls.push([method, request]);
|
|
20
|
-
if (method === "
|
|
19
|
+
if (method === "getVersion")
|
|
20
|
+
return { version: "9.9.9", commit: "abc" };
|
|
21
|
+
if (method === "listContainers") {
|
|
21
22
|
return { containers: [{ containerName: "c1" }] };
|
|
23
|
+
}
|
|
22
24
|
if (method === "listImages")
|
|
23
25
|
return { images: [{ imageId: "img1" }] };
|
|
24
26
|
if (method === "listWorkspaces")
|
|
@@ -27,8 +29,9 @@ function fakeResolver() {
|
|
|
27
29
|
return { volumes: [{ name: "data" }] };
|
|
28
30
|
if (method === "listSecrets")
|
|
29
31
|
return { secrets: [{ name: "s1" }] };
|
|
30
|
-
if (method === "listCaches")
|
|
32
|
+
if (method === "listCaches") {
|
|
31
33
|
return { caches: [{ manager: "pacman", files: 1, bytes: "1024" }] };
|
|
34
|
+
}
|
|
32
35
|
if (method === "cleanCaches")
|
|
33
36
|
return { removedFiles: 2 };
|
|
34
37
|
if (method === "removeContainer")
|
|
@@ -42,7 +45,7 @@ function fakeRouteContext() {
|
|
|
42
45
|
const routes = [];
|
|
43
46
|
const ctx = {
|
|
44
47
|
inject(deps, callback) {
|
|
45
|
-
assert.deepEqual(deps, ["connection"
|
|
48
|
+
assert.deepEqual(deps, ["connection"]);
|
|
46
49
|
callback({
|
|
47
50
|
connection: {
|
|
48
51
|
fetch: {
|
|
@@ -57,10 +60,6 @@ function fakeRouteContext() {
|
|
|
57
60
|
},
|
|
58
61
|
},
|
|
59
62
|
},
|
|
60
|
-
settings: {
|
|
61
|
-
register: () => ({ watch: () => () => { } }),
|
|
62
|
-
get: () => undefined,
|
|
63
|
-
},
|
|
64
63
|
});
|
|
65
64
|
},
|
|
66
65
|
};
|
|
@@ -92,7 +91,38 @@ test("the card route serves the live snapshot on GET", async () => {
|
|
|
92
91
|
assert.deepEqual(snapshot.volumes, [{ name: "data" }]);
|
|
93
92
|
assert.deepEqual(snapshot.secrets, [{ name: "s1" }]);
|
|
94
93
|
assert.equal(snapshot.caches[0].bytes, 1024);
|
|
95
|
-
assert.deepEqual(calls.map(([method]) => method), [
|
|
94
|
+
assert.deepEqual(calls.map(([method]) => method), [
|
|
95
|
+
"getVersion",
|
|
96
|
+
"listContainers",
|
|
97
|
+
"listImages",
|
|
98
|
+
"listWorkspaces",
|
|
99
|
+
"listVolumes",
|
|
100
|
+
"listSecrets",
|
|
101
|
+
"listCaches",
|
|
102
|
+
]);
|
|
103
|
+
assert.equal(typeof snapshot.dshVersion, "string");
|
|
104
|
+
assert.equal(snapshot.orchestratorVersion, "9.9.9");
|
|
105
|
+
assert.equal(snapshot.versionState, "major-mismatch", "a differing major is surfaced on the card");
|
|
106
|
+
});
|
|
107
|
+
test("a refused control call reports the version mismatch instead of an empty snapshot", async () => {
|
|
108
|
+
const { resolver } = fakeResolver();
|
|
109
|
+
const refusal = new Error("plugin is incompatible");
|
|
110
|
+
refusal.code = "FailedPrecondition";
|
|
111
|
+
resolver.control = async (method) => {
|
|
112
|
+
if (method === "getVersion")
|
|
113
|
+
return { version: "9.9.9", commit: "abc" };
|
|
114
|
+
throw refusal;
|
|
115
|
+
};
|
|
116
|
+
const { ctx, routes } = fakeRouteContext();
|
|
117
|
+
registerCardRoute(ctx, resolver);
|
|
118
|
+
const response = await routes[0].fetch(cardRequest());
|
|
119
|
+
assert.equal(response.status, 200);
|
|
120
|
+
const snapshot = (await response.json());
|
|
121
|
+
assert.equal(typeof snapshot.dshVersion, "string");
|
|
122
|
+
assert.equal(snapshot.orchestratorVersion, "9.9.9");
|
|
123
|
+
assert.equal(snapshot.versionState, "major-mismatch");
|
|
124
|
+
assert.deepEqual(snapshot.containers, []);
|
|
125
|
+
assert.deepEqual(snapshot.workspaces, []);
|
|
96
126
|
});
|
|
97
127
|
test("the card route runs one command per POST and returns its notice", async () => {
|
|
98
128
|
const { resolver, calls } = fakeResolver();
|
|
@@ -104,9 +134,51 @@ test("the card route runs one command per POST and returns its notice", async ()
|
|
|
104
134
|
}));
|
|
105
135
|
assert.equal(remove.status, 200);
|
|
106
136
|
assert.deepEqual(await remove.json(), {});
|
|
107
|
-
|
|
137
|
+
// A command that names no container keeps the orchestrator's default; one that
|
|
138
|
+
// names a container must reach it, or a row's Remove would hit another one.
|
|
139
|
+
const removeNamed = await routes[0].fetch(cardRequest({
|
|
140
|
+
method: "POST",
|
|
141
|
+
body: JSON.stringify({
|
|
142
|
+
command: { op: "remove", workspace: "w1", container: "foo" },
|
|
143
|
+
}),
|
|
144
|
+
}));
|
|
145
|
+
assert.equal(removeNamed.status, 200);
|
|
146
|
+
const removeCalls = calls.filter(([method]) => method === "removeContainer");
|
|
147
|
+
assert.deepEqual(removeCalls[0]?.[1], { workspaceSlug: "w1" });
|
|
148
|
+
assert.deepEqual(removeCalls[1]?.[1], {
|
|
149
|
+
workspaceSlug: "w1",
|
|
150
|
+
container: "foo",
|
|
151
|
+
});
|
|
152
|
+
const recreate = await routes[0].fetch(cardRequest({
|
|
153
|
+
method: "POST",
|
|
154
|
+
body: JSON.stringify({
|
|
155
|
+
command: {
|
|
156
|
+
op: "recreate",
|
|
157
|
+
workspace: "w1",
|
|
158
|
+
container: "foo",
|
|
159
|
+
image: "img1",
|
|
160
|
+
},
|
|
161
|
+
}),
|
|
162
|
+
}));
|
|
163
|
+
assert.equal(recreate.status, 200);
|
|
164
|
+
assert.deepEqual(calls.find(([method]) => method === "recreateContainer")?.[1], {
|
|
108
165
|
workspaceSlug: "w1",
|
|
166
|
+
container: "foo",
|
|
167
|
+
imageId: "img1",
|
|
109
168
|
});
|
|
169
|
+
const detach = await routes[0].fetch(cardRequest({
|
|
170
|
+
method: "POST",
|
|
171
|
+
body: JSON.stringify({
|
|
172
|
+
command: {
|
|
173
|
+
op: "container_secret_remove",
|
|
174
|
+
workspace: "w1",
|
|
175
|
+
container: "foo",
|
|
176
|
+
secretEnvName: "TOKEN",
|
|
177
|
+
},
|
|
178
|
+
}),
|
|
179
|
+
}));
|
|
180
|
+
assert.equal(detach.status, 200);
|
|
181
|
+
assert.deepEqual(calls.find(([method]) => method === "removeContainerSecret")?.[1], { workspaceSlug: "w1", container: "foo", env: "TOKEN" });
|
|
110
182
|
const clean = await routes[0].fetch(cardRequest({
|
|
111
183
|
method: "POST",
|
|
112
184
|
body: JSON.stringify({ command: { op: "cache_clean", cacheMode: "all" } }),
|
|
@@ -145,37 +217,7 @@ test("the card route reports a failed command as an error", async () => {
|
|
|
145
217
|
body: JSON.stringify({ command: { op: "remove", workspace: "w1" } }),
|
|
146
218
|
}));
|
|
147
219
|
assert.equal(response.status, 500);
|
|
148
|
-
assert.deepEqual(await response.json(), {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
let base;
|
|
152
|
-
let watcher;
|
|
153
|
-
const ctx = {
|
|
154
|
-
inject(deps, callback) {
|
|
155
|
-
assert.deepEqual(deps, ["settings"]);
|
|
156
|
-
callback({
|
|
157
|
-
settings: {
|
|
158
|
-
register(_ns, _schema, options) {
|
|
159
|
-
base = options.base;
|
|
160
|
-
return {
|
|
161
|
-
watch(callback) {
|
|
162
|
-
watcher = callback;
|
|
163
|
-
return () => { };
|
|
164
|
-
},
|
|
165
|
-
};
|
|
166
|
-
},
|
|
167
|
-
},
|
|
168
|
-
});
|
|
169
|
-
},
|
|
170
|
-
};
|
|
171
|
-
const configs = [];
|
|
172
|
-
const resolver = {
|
|
173
|
-
getConfig: () => ({ defaultImage: "archlinux", socketsRoot: "/run/dsh-podman" }),
|
|
174
|
-
setConfig: (patch) => configs.push(patch),
|
|
175
|
-
};
|
|
176
|
-
installContainerPreferences(ctx, resolver);
|
|
177
|
-
assert.deepEqual(base, { defaultImage: "archlinux", socketsRoot: "/run/dsh-podman" });
|
|
178
|
-
assert.ok(watcher !== undefined);
|
|
179
|
-
watcher({ defaultImage: "ubuntu", socketsRoot: "/run/other" });
|
|
180
|
-
assert.deepEqual(configs, [{ defaultImage: "ubuntu", socketsRoot: "/run/other" }]);
|
|
220
|
+
assert.deepEqual(await response.json(), {
|
|
221
|
+
error: "control plane unavailable",
|
|
222
|
+
});
|
|
181
223
|
});
|
|
@@ -6,8 +6,6 @@
|
|
|
6
6
|
// `runCommand` and folds the real `cardSnapshot` back into the fake scope, so
|
|
7
7
|
// the command tests stay focused on the command-to-control mapping. The HTTP
|
|
8
8
|
// layer itself is covered by card-route.test.ts.
|
|
9
|
-
import { strict as assert } from "node:assert";
|
|
10
|
-
import { CONTAINER_NS, settingsSchema } from "./settings-schema.js";
|
|
11
9
|
import { cardSnapshot, runCommand } from "./card-route.js";
|
|
12
10
|
export function fakeScope(initial) {
|
|
13
11
|
const scope = {
|
|
@@ -30,26 +28,14 @@ export function fakeScope(initial) {
|
|
|
30
28
|
return scope;
|
|
31
29
|
}
|
|
32
30
|
export function fakeContext(scope) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
settings: {
|
|
38
|
-
register() {
|
|
39
|
-
return scope;
|
|
40
|
-
},
|
|
41
|
-
get() {
|
|
42
|
-
return undefined;
|
|
43
|
-
},
|
|
44
|
-
},
|
|
45
|
-
});
|
|
46
|
-
},
|
|
47
|
-
};
|
|
31
|
+
// The host half no longer registers a settings namespace: the card's
|
|
32
|
+
// preferences are volatile plugin-config fields, so the fake document is
|
|
33
|
+
// handed to the driver directly.
|
|
34
|
+
return { fakeScope: scope };
|
|
48
35
|
}
|
|
49
36
|
export function baseValue() {
|
|
50
37
|
return {
|
|
51
38
|
defaultImage: "archlinux",
|
|
52
|
-
socketsRoot: "/run/dsh-podman",
|
|
53
39
|
uiLocale: "",
|
|
54
40
|
notice: "",
|
|
55
41
|
command: null,
|
|
@@ -61,62 +47,49 @@ export function baseValue() {
|
|
|
61
47
|
caches: [],
|
|
62
48
|
};
|
|
63
49
|
}
|
|
64
|
-
// installCardCommandDriver wires a fake settings
|
|
50
|
+
// installCardCommandDriver wires a fake settings document to the real command
|
|
65
51
|
// runner and snapshot builder, standing in for the browser transport: it
|
|
66
52
|
// mirrors what the card does per request — run the command, then refresh the
|
|
67
53
|
// live snapshot and record the notice.
|
|
68
54
|
export function installCardCommandDriver(ctx, resolver, registry) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
55
|
+
const scope = ctx.fakeScope;
|
|
56
|
+
const refresh = async (notice = "") => {
|
|
57
|
+
let view = {};
|
|
58
|
+
try {
|
|
59
|
+
const snapshot = await cardSnapshot(resolver, registry);
|
|
60
|
+
view = {
|
|
61
|
+
workspaces: snapshot.workspaces,
|
|
62
|
+
containers: snapshot.containers,
|
|
63
|
+
images: snapshot.images,
|
|
64
|
+
volumes: snapshot.volumes,
|
|
65
|
+
secrets: snapshot.secrets,
|
|
66
|
+
caches: snapshot.caches,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Command tests do not always stub every list call.
|
|
71
|
+
}
|
|
72
|
+
await scope.update({ ...view, notice });
|
|
73
|
+
};
|
|
74
|
+
scope.watch((next) => {
|
|
75
|
+
const command = next.command;
|
|
76
|
+
if (command === null || command === undefined)
|
|
77
|
+
return;
|
|
78
|
+
void (async () => {
|
|
79
|
+
// Clear the command before running it, so the refresh below cannot
|
|
80
|
+
// re-deliver it.
|
|
81
|
+
await scope.update({ command: null });
|
|
82
|
+
let notice = "";
|
|
79
83
|
try {
|
|
80
|
-
|
|
81
|
-
view = {
|
|
82
|
-
workspaces: snapshot.workspaces,
|
|
83
|
-
containers: snapshot.containers,
|
|
84
|
-
images: snapshot.images,
|
|
85
|
-
volumes: snapshot.volumes,
|
|
86
|
-
secrets: snapshot.secrets,
|
|
87
|
-
caches: snapshot.caches,
|
|
88
|
-
};
|
|
84
|
+
notice = await runCommand(ctx, resolver, registry, command);
|
|
89
85
|
}
|
|
90
|
-
catch {
|
|
91
|
-
|
|
86
|
+
catch (error) {
|
|
87
|
+
notice = error instanceof Error ? error.message : String(error);
|
|
92
88
|
}
|
|
93
|
-
await
|
|
94
|
-
};
|
|
95
|
-
scope.watch((next) => {
|
|
96
|
-
resolver.setConfig({
|
|
97
|
-
defaultImage: next.defaultImage,
|
|
98
|
-
socketsRoot: next.socketsRoot,
|
|
99
|
-
});
|
|
100
|
-
const command = next.command;
|
|
101
|
-
if (command === null || command === undefined)
|
|
102
|
-
return;
|
|
103
|
-
void (async () => {
|
|
104
|
-
// Clear the command before running it, so the refresh below cannot
|
|
105
|
-
// re-deliver it.
|
|
106
|
-
await scope.update({ command: null });
|
|
107
|
-
let notice = "";
|
|
108
|
-
try {
|
|
109
|
-
notice = await runCommand(sctx, resolver, registry, command);
|
|
110
|
-
}
|
|
111
|
-
catch (error) {
|
|
112
|
-
notice = error instanceof Error ? error.message : String(error);
|
|
113
|
-
}
|
|
114
|
-
await refresh(notice);
|
|
115
|
-
})();
|
|
116
|
-
});
|
|
117
|
-
installed = refresh();
|
|
89
|
+
await refresh(notice);
|
|
90
|
+
})();
|
|
118
91
|
});
|
|
119
|
-
return
|
|
92
|
+
return refresh();
|
|
120
93
|
}
|
|
121
94
|
export async function installedMountScope() {
|
|
122
95
|
const scope = fakeScope(baseValue());
|