@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
|
@@ -10,9 +10,22 @@ import { dirname, resolve } from "node:path";
|
|
|
10
10
|
import { tmpdir } from "node:os";
|
|
11
11
|
import { mkdirSync } from "node:fs";
|
|
12
12
|
import { controlClient, guestClient } from "./grpc/runtime-client.js";
|
|
13
|
-
import {
|
|
13
|
+
import { containerNotFound, metadata, normalizeToolError, WorkspaceResolver, workspaceSlug, } from "./workspace-binding.js";
|
|
14
|
+
import { VERSION } from "./generated/version.js";
|
|
14
15
|
const SLUG = "2c573001-4171-4900-904b-12a5cc02737a";
|
|
15
16
|
const SLUG_SUB = "3d684112-5282-4a11-a15c-23b6dd13848b";
|
|
17
|
+
test("normalizeToolError strips the prefix only for a gRPC status", () => {
|
|
18
|
+
const statusError = Object.assign(new Error("5 NOT_FOUND: missing"), {
|
|
19
|
+
code: 5,
|
|
20
|
+
});
|
|
21
|
+
const normalized = normalizeToolError(statusError);
|
|
22
|
+
assert.equal(normalized.message, "missing");
|
|
23
|
+
assert.equal(normalized.code, "NOT_FOUND");
|
|
24
|
+
// A plain message that merely looks like a status keeps its text.
|
|
25
|
+
const plain = normalizeToolError(new Error("5 NOT_FOUND: missing"));
|
|
26
|
+
assert.equal(plain.message, "5 NOT_FOUND: missing");
|
|
27
|
+
assert.equal(plain.code, undefined);
|
|
28
|
+
});
|
|
16
29
|
test("workspaceSlug accepts a UUID workspace id", () => {
|
|
17
30
|
assert.equal(workspaceSlug(SLUG), SLUG);
|
|
18
31
|
assert.equal(workspaceSlug(SLUG.toUpperCase()), SLUG);
|
|
@@ -44,6 +57,12 @@ test("metadata omits the header when the token is empty", () => {
|
|
|
44
57
|
assert.ok(result instanceof grpc.Metadata);
|
|
45
58
|
assert.equal(result.get("authorization").length, 0);
|
|
46
59
|
});
|
|
60
|
+
test("metadata carries the plugin version only when one is given", () => {
|
|
61
|
+
const withVersion = metadata("token-1", "1.2.3");
|
|
62
|
+
assert.equal(withVersion.get("x-dsh-podman-plugin-version")[0], "1.2.3");
|
|
63
|
+
// Guest calls pass no version, and the guest agent does not check it.
|
|
64
|
+
assert.equal(metadata("token-1").get("x-dsh-podman-plugin-version").length, 0);
|
|
65
|
+
});
|
|
47
66
|
test("normalizeToolError strips the grpc prefix and names the code", () => {
|
|
48
67
|
const prefixed = Object.assign(new Error("5 NOT_FOUND: container not found"), { code: 5 });
|
|
49
68
|
const normalized = normalizeToolError(prefixed);
|
|
@@ -52,7 +71,9 @@ test("normalizeToolError strips the grpc prefix and names the code", () => {
|
|
|
52
71
|
const plain = normalizeToolError(new Error("boom"));
|
|
53
72
|
assert.equal(plain.message, "boom");
|
|
54
73
|
assert.equal(plain.code, undefined);
|
|
55
|
-
const named = Object.assign(new Error("bad input"), {
|
|
74
|
+
const named = Object.assign(new Error("bad input"), {
|
|
75
|
+
code: "INVALID_ARGUMENT",
|
|
76
|
+
});
|
|
56
77
|
assert.equal(normalizeToolError(named).code, "INVALID_ARGUMENT");
|
|
57
78
|
});
|
|
58
79
|
test("containerNotFound reports one stable shape", () => {
|
|
@@ -60,25 +81,6 @@ test("containerNotFound reports one stable shape", () => {
|
|
|
60
81
|
assert.equal(containerNotFound("db").code, "NOT_FOUND");
|
|
61
82
|
assert.equal(containerNotFound("db").message, 'container "db" not found');
|
|
62
83
|
});
|
|
63
|
-
test("containerRowFor finds rows by workspace and name", () => {
|
|
64
|
-
const containers = [
|
|
65
|
-
{ workspaceSlug: "team-app", containerName: "default", status: "running" },
|
|
66
|
-
{ workspaceSlug: "team-app", containerName: "db", status: "stopped" },
|
|
67
|
-
{ workspaceSlug: "other", containerName: "db", status: "running" },
|
|
68
|
-
];
|
|
69
|
-
assert.deepEqual(containerRowFor(containers, "team-app", "db"), {
|
|
70
|
-
workspaceSlug: "team-app",
|
|
71
|
-
containerName: "db",
|
|
72
|
-
status: "stopped",
|
|
73
|
-
});
|
|
74
|
-
assert.deepEqual(containerRowFor(containers, "team-app", "default"), {
|
|
75
|
-
workspaceSlug: "team-app",
|
|
76
|
-
containerName: "default",
|
|
77
|
-
status: "running",
|
|
78
|
-
});
|
|
79
|
-
assert.equal(containerRowFor(containers, "team-app", "missing"), undefined);
|
|
80
|
-
assert.equal(containerRowFor([], "team-app", "default"), undefined);
|
|
81
|
-
});
|
|
82
84
|
test("control and guest proto files resolve next to the runtime", () => {
|
|
83
85
|
const control = controlClient("/tmp/dsh-proto-control.sock");
|
|
84
86
|
const guest = guestClient("/tmp/dsh-proto-guest.sock");
|
|
@@ -93,20 +95,60 @@ async function startControlServer(containers = [], options = {}) {
|
|
|
93
95
|
defaults: true,
|
|
94
96
|
});
|
|
95
97
|
const loaded = grpc.loadPackageDefinition(definition);
|
|
98
|
+
const guestDefinition = loader.loadSync(resolve(dirname(fileURLToPath(import.meta.url)), "grpc/proto/dshguest/v1/guest.proto"), { longs: String, enums: String, defaults: true });
|
|
99
|
+
const guestLoaded = grpc.loadPackageDefinition(guestDefinition);
|
|
96
100
|
const server = new grpc.Server();
|
|
101
|
+
// With staleToken, the first container row carries a credential the agent
|
|
102
|
+
// rejects, so the caller must refresh before it can proceed.
|
|
103
|
+
let ensureCalls = 0;
|
|
104
|
+
server.addService(guestLoaded.dshguest.v1.WorkspaceGuestAgent.service, {
|
|
105
|
+
ping: (call, callback) => {
|
|
106
|
+
const bearer = String(call.metadata.get("authorization")[0] ?? "");
|
|
107
|
+
if (bearer === "bearer stale") {
|
|
108
|
+
callback({
|
|
109
|
+
code: grpc.status.UNAUTHENTICATED,
|
|
110
|
+
details: "invalid agent token",
|
|
111
|
+
});
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
callback(null, { version: "test", commit: "test" });
|
|
115
|
+
},
|
|
116
|
+
});
|
|
97
117
|
const received = [];
|
|
118
|
+
const versions = [];
|
|
98
119
|
const createRequests = [];
|
|
99
120
|
const recreateRequests = [];
|
|
100
121
|
server.addService(loaded.dshctl.v1.OrchestratorControl.service, {
|
|
101
122
|
listWorkspaces: (call, callback) => {
|
|
102
123
|
received.push(call.metadata.get("authorization")[0]);
|
|
124
|
+
versions.push(call.metadata.get("x-dsh-podman-plugin-version")[0]);
|
|
103
125
|
callback(null, { workspaces: [] });
|
|
104
126
|
},
|
|
105
127
|
describeWorkspace: (_call, callback) => {
|
|
106
128
|
callback({ code: grpc.status.NOT_FOUND, details: "workspace not found" });
|
|
107
129
|
},
|
|
108
|
-
|
|
109
|
-
|
|
130
|
+
ensureContainer: (call, callback) => {
|
|
131
|
+
const agentToken = options.staleToken === true && ensureCalls++ === 0
|
|
132
|
+
? "stale"
|
|
133
|
+
: "tok";
|
|
134
|
+
const row = containers.find((candidate) => candidate.workspaceSlug === call.request.workspaceSlug &&
|
|
135
|
+
candidate.containerName === call.request.container);
|
|
136
|
+
if (row !== undefined) {
|
|
137
|
+
callback(null, { ...row, agentToken });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (call.request.container === "default") {
|
|
141
|
+
// The default container createWorkspace just made.
|
|
142
|
+
callback(null, {
|
|
143
|
+
workspaceSlug: call.request.workspaceSlug,
|
|
144
|
+
containerName: "default",
|
|
145
|
+
agentSocketPath: resolve(socketsRoot, "guest.sock"),
|
|
146
|
+
agentToken,
|
|
147
|
+
mounts: [],
|
|
148
|
+
});
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
callback({ code: grpc.status.NOT_FOUND, details: "container not found" });
|
|
110
152
|
},
|
|
111
153
|
createWorkspace: (call, callback) => {
|
|
112
154
|
createRequests.push(call.request);
|
|
@@ -120,6 +162,12 @@ async function startControlServer(containers = [], options = {}) {
|
|
|
120
162
|
recreateRequests.push(call.request);
|
|
121
163
|
callback(null, { workspaceSlug: call.request.workspaceSlug });
|
|
122
164
|
},
|
|
165
|
+
addContainerMount: (_call, callback) => {
|
|
166
|
+
callback(null, {});
|
|
167
|
+
},
|
|
168
|
+
removeWorkspace: (_call, callback) => {
|
|
169
|
+
callback(null, {});
|
|
170
|
+
},
|
|
123
171
|
});
|
|
124
172
|
const socketsRoot = resolve(tmpdir(), `dsh-control-test-${process.pid}-${Date.now()}-${Math.random()}`);
|
|
125
173
|
mkdirSync(socketsRoot, { recursive: true });
|
|
@@ -139,13 +187,14 @@ async function startControlServer(containers = [], options = {}) {
|
|
|
139
187
|
return {
|
|
140
188
|
socketsRoot,
|
|
141
189
|
received,
|
|
190
|
+
versions,
|
|
142
191
|
createRequests,
|
|
143
192
|
recreateRequests,
|
|
144
193
|
stop: () => server.forceShutdown(),
|
|
145
194
|
};
|
|
146
195
|
}
|
|
147
196
|
test("control calls carry the bearer token", async () => {
|
|
148
|
-
const { socketsRoot, received, stop } = await startControlServer();
|
|
197
|
+
const { socketsRoot, received, versions, stop } = await startControlServer();
|
|
149
198
|
try {
|
|
150
199
|
const resolver = new WorkspaceResolver({
|
|
151
200
|
socketsRoot,
|
|
@@ -155,13 +204,14 @@ test("control calls carry the bearer token", async () => {
|
|
|
155
204
|
}, undefined);
|
|
156
205
|
await resolver.control("listWorkspaces", {});
|
|
157
206
|
assert.equal(received[0], "bearer tok-1");
|
|
207
|
+
assert.equal(versions[0], VERSION, "control calls report the plugin version");
|
|
158
208
|
}
|
|
159
209
|
finally {
|
|
160
210
|
stop();
|
|
161
211
|
}
|
|
162
212
|
});
|
|
163
213
|
test("control calls omit the token when unset", async () => {
|
|
164
|
-
const { socketsRoot, received, stop } = await startControlServer();
|
|
214
|
+
const { socketsRoot, received, versions, stop } = await startControlServer();
|
|
165
215
|
try {
|
|
166
216
|
const resolver = new WorkspaceResolver({
|
|
167
217
|
socketsRoot,
|
|
@@ -215,6 +265,60 @@ test("resolve exposes the session directory as the default cwd", async () => {
|
|
|
215
265
|
stop();
|
|
216
266
|
}
|
|
217
267
|
});
|
|
268
|
+
test("resolve refreshes a binding whose token the agent rejects", async () => {
|
|
269
|
+
// The first row carries a credential the guest agent rejects; resolving must
|
|
270
|
+
// re-run the orchestrator's ensure and end with the usable token.
|
|
271
|
+
const { socketsRoot, stop } = await startControlServer([], {
|
|
272
|
+
staleToken: true,
|
|
273
|
+
});
|
|
274
|
+
try {
|
|
275
|
+
const resolver = new WorkspaceResolver({
|
|
276
|
+
socketsRoot,
|
|
277
|
+
defaultImage: "arch",
|
|
278
|
+
projectsRoot: "/projects",
|
|
279
|
+
controlToken: "",
|
|
280
|
+
}, { resolveByPath: () => ({ id: SLUG, path: "/projects/team" }) });
|
|
281
|
+
const binding = await resolver.resolve("/projects/team");
|
|
282
|
+
assert.equal(binding.token, "tok");
|
|
283
|
+
}
|
|
284
|
+
finally {
|
|
285
|
+
stop();
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
test("the auto-created workspace seeds the default environment", async () => {
|
|
289
|
+
const { socketsRoot, createRequests, stop } = await startControlServer();
|
|
290
|
+
try {
|
|
291
|
+
const resolver = new WorkspaceResolver({
|
|
292
|
+
socketsRoot,
|
|
293
|
+
defaultImage: "arch",
|
|
294
|
+
projectsRoot: "/projects",
|
|
295
|
+
controlToken: "",
|
|
296
|
+
containerEnv: { GIT_AUTHOR_NAME: "John Doe" },
|
|
297
|
+
}, { resolveByPath: () => ({ id: SLUG, path: "/projects/team" }) });
|
|
298
|
+
await resolver.resolve("/projects/team");
|
|
299
|
+
assert.equal(createRequests.length, 1);
|
|
300
|
+
assert.deepEqual(createRequests[0].env, { GIT_AUTHOR_NAME: "John Doe" });
|
|
301
|
+
}
|
|
302
|
+
finally {
|
|
303
|
+
stop();
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
test("the auto-created workspace omits env without defaults", async () => {
|
|
307
|
+
const { socketsRoot, createRequests, stop } = await startControlServer();
|
|
308
|
+
try {
|
|
309
|
+
const resolver = new WorkspaceResolver({
|
|
310
|
+
socketsRoot,
|
|
311
|
+
defaultImage: "arch",
|
|
312
|
+
projectsRoot: "/projects",
|
|
313
|
+
controlToken: "",
|
|
314
|
+
}, { resolveByPath: () => ({ id: SLUG, path: "/projects/team" }) });
|
|
315
|
+
await resolver.resolve("/projects/team");
|
|
316
|
+
assert.deepEqual(createRequests[0].env, {});
|
|
317
|
+
}
|
|
318
|
+
finally {
|
|
319
|
+
stop();
|
|
320
|
+
}
|
|
321
|
+
});
|
|
218
322
|
test("containerBinding exposes the session directory only when a project mount covers it", async () => {
|
|
219
323
|
const mounted = await startControlServer([{
|
|
220
324
|
workspaceSlug: SLUG,
|
|
@@ -241,7 +345,11 @@ test("containerBinding exposes the session directory only when a project mount c
|
|
|
241
345
|
containerName: "db",
|
|
242
346
|
agentSocketPath: "/run/x.sock",
|
|
243
347
|
agentToken: "tok",
|
|
244
|
-
mounts: [{
|
|
348
|
+
mounts: [{
|
|
349
|
+
kind: "MOUNT_KIND_VOLUME",
|
|
350
|
+
volume: "data",
|
|
351
|
+
destination: "/data",
|
|
352
|
+
}],
|
|
245
353
|
}]);
|
|
246
354
|
try {
|
|
247
355
|
const resolver = new WorkspaceResolver({
|
|
@@ -257,6 +365,72 @@ test("containerBinding exposes the session directory only when a project mount c
|
|
|
257
365
|
unmounted.stop();
|
|
258
366
|
}
|
|
259
367
|
});
|
|
368
|
+
test("containerBinding re-resolves the mount set after a container mutation", async () => {
|
|
369
|
+
const rows = [
|
|
370
|
+
{
|
|
371
|
+
workspaceSlug: SLUG,
|
|
372
|
+
containerName: "db",
|
|
373
|
+
agentSocketPath: "/run/x.sock",
|
|
374
|
+
agentToken: "tok",
|
|
375
|
+
mounts: [{ kind: "MOUNT_KIND_PROJECT", projectName: "team" }],
|
|
376
|
+
},
|
|
377
|
+
];
|
|
378
|
+
const control = await startControlServer(rows);
|
|
379
|
+
try {
|
|
380
|
+
const resolver = new WorkspaceResolver({
|
|
381
|
+
socketsRoot: control.socketsRoot,
|
|
382
|
+
defaultImage: "arch",
|
|
383
|
+
projectsRoot: "/projects",
|
|
384
|
+
controlToken: "",
|
|
385
|
+
}, { resolveByPath: () => ({ id: SLUG, path: "/projects/team" }) });
|
|
386
|
+
const first = await resolver.containerBinding("/projects/team", "db");
|
|
387
|
+
assert.equal(first.defaultCwd, "/projects/team");
|
|
388
|
+
// The container no longer mounts the session directory, but the cached
|
|
389
|
+
// entry still describes the row it was built from.
|
|
390
|
+
rows[0].mounts = [
|
|
391
|
+
{ kind: "MOUNT_KIND_VOLUME", volume: "data", destination: "/data" },
|
|
392
|
+
];
|
|
393
|
+
const stale = await resolver.containerBinding("/projects/team", "db");
|
|
394
|
+
assert.equal(stale.defaultCwd, "/projects/team", "the cache is served until it is invalidated");
|
|
395
|
+
await resolver.control("addContainerMount", {
|
|
396
|
+
workspaceSlug: SLUG,
|
|
397
|
+
container: "db",
|
|
398
|
+
});
|
|
399
|
+
const fresh = await resolver.containerBinding("/projects/team", "db");
|
|
400
|
+
assert.equal(fresh.defaultCwd, undefined, "a mutation re-resolves the mount set");
|
|
401
|
+
}
|
|
402
|
+
finally {
|
|
403
|
+
control.stop();
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
test("removing a workspace drops every cached container binding", async () => {
|
|
407
|
+
const rows = [
|
|
408
|
+
{
|
|
409
|
+
workspaceSlug: SLUG,
|
|
410
|
+
containerName: "db",
|
|
411
|
+
agentSocketPath: "/run/x.sock",
|
|
412
|
+
agentToken: "tok",
|
|
413
|
+
mounts: [{ kind: "MOUNT_KIND_PROJECT", projectName: "team" }],
|
|
414
|
+
},
|
|
415
|
+
];
|
|
416
|
+
const control = await startControlServer(rows);
|
|
417
|
+
try {
|
|
418
|
+
const resolver = new WorkspaceResolver({
|
|
419
|
+
socketsRoot: control.socketsRoot,
|
|
420
|
+
defaultImage: "arch",
|
|
421
|
+
projectsRoot: "/projects",
|
|
422
|
+
controlToken: "",
|
|
423
|
+
}, { resolveByPath: () => ({ id: SLUG, path: "/projects/team" }) });
|
|
424
|
+
await resolver.containerBinding("/projects/team", "db");
|
|
425
|
+
rows[0].mounts = [];
|
|
426
|
+
await resolver.control("removeWorkspace", { workspaceSlug: SLUG });
|
|
427
|
+
const fresh = await resolver.containerBinding("/projects/team", "db");
|
|
428
|
+
assert.equal(fresh.defaultCwd, undefined);
|
|
429
|
+
}
|
|
430
|
+
finally {
|
|
431
|
+
control.stop();
|
|
432
|
+
}
|
|
433
|
+
});
|
|
260
434
|
test("containerBinding recreates a container whose agent never answers", async () => {
|
|
261
435
|
const control = await startControlServer([{
|
|
262
436
|
workspaceSlug: SLUG,
|
|
@@ -320,7 +494,8 @@ test("resolveForPath rejects paths it cannot map to a workspace", async () => {
|
|
|
320
494
|
// A path outside every workspace is not in any container's filesystem: it is
|
|
321
495
|
// reported with the harness's missing-path code so callers treat it as absent
|
|
322
496
|
// (agent-instructions walks ancestors above the workspace this way).
|
|
323
|
-
await assert.rejects(() => resolver.resolveForPath("/elsewhere/file", undefined), (error) => error instanceof Error &&
|
|
497
|
+
await assert.rejects(() => resolver.resolveForPath("/elsewhere/file", undefined), (error) => error instanceof Error &&
|
|
498
|
+
error.code === "FS_NOT_FOUND");
|
|
324
499
|
});
|
|
325
500
|
test("resolve rejects a missing cwd without stringifying it", async () => {
|
|
326
501
|
const resolver = new WorkspaceResolver({
|
package/docs/architecture.md
CHANGED
|
@@ -24,9 +24,22 @@ The plugin connects to the orchestrator over a Unix socket at
|
|
|
24
24
|
`<socketsRoot>/orchestrator.sock` and talks gRPC. Requests carry the shared
|
|
25
25
|
token (`DSH_PODMAN_ORCHESTRATOR_TOKEN`) as an `authorization: bearer` header.
|
|
26
26
|
|
|
27
|
+
Requests also carry the plugin's version in an `x-dsh-podman-plugin-version`
|
|
28
|
+
header. The plugin and the orchestrator are deployed separately (an npm package
|
|
29
|
+
inside the dsh image, and this binary), so the orchestrator refuses a control
|
|
30
|
+
call from a plugin whose **major** version differs, naming both versions and
|
|
31
|
+
which side is behind — an out-of-sync deployment fails loudly instead of being
|
|
32
|
+
misread. A missing or unparseable version is refused as well. A minor or patch
|
|
33
|
+
difference is compatible: the call proceeds, the orchestrator logs it once per
|
|
34
|
+
plugin version, and the Podman page shows it. `GetVersion` is exempt from the
|
|
35
|
+
check, so a plugin can always learn the orchestrator's version.
|
|
36
|
+
|
|
27
37
|
The orchestrator service exposes these gRPC methods (backing both the UI and the
|
|
28
38
|
tools): `ListContainers` (returns only the guest containers the orchestrator
|
|
29
39
|
created — containers it does not own are never exposed),
|
|
40
|
+
`EnsureContainer{workspace_slug, container}` (the single attach path: returns
|
|
41
|
+
the named container, recreating it first when it is missing, stopped, or running
|
|
42
|
+
an outdated guest agent),
|
|
30
43
|
`StartContainer{workspace_slug, container, image_id, mounts, env, secret_env,
|
|
31
44
|
paths}`
|
|
32
45
|
(creates or replaces a container in the workspace's pod; an empty `container`
|
|
@@ -35,8 +48,11 @@ targets the default container and other names are validated),
|
|
|
35
48
|
(stops, removes, and recreates a container, optionally with a new image, project
|
|
36
49
|
mounts, environment, or PATH additions; an empty `image_id` keeps the
|
|
37
50
|
workspace's current image), `RemoveContainer`, `AddContainerMount`,
|
|
38
|
-
`
|
|
39
|
-
`RemoveContainerSecret`.
|
|
51
|
+
`UpdateContainerMount`, `RemoveContainerMount`, `SetContainerPaths`,
|
|
52
|
+
`AddContainerSecret`, and `RemoveContainerSecret`. The service also exposes the
|
|
53
|
+
workspace, volume, secret, image (build, rebuild, remove and base-image pull),
|
|
54
|
+
and cache (list and clean) operations that back the Podman page, plus
|
|
55
|
+
`GetVersion`.
|
|
40
56
|
|
|
41
57
|
## Workspaces and pods
|
|
42
58
|
|
|
@@ -55,10 +71,10 @@ model reads the artifact back with the container file tools instead of a host
|
|
|
55
71
|
path the container cannot reach.
|
|
56
72
|
|
|
57
73
|
A workspace's pod is torn down when its last container is removed, or directly
|
|
58
|
-
through `RemoveWorkspace` (the
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
74
|
+
through `RemoveWorkspace` (the Podman page's **Remove pod** action), which stops
|
|
75
|
+
the containers' daemons, removes the pod and its containers, cleans their socket
|
|
76
|
+
directories, and drops the stored workspace. Volumes, secrets, and project data
|
|
77
|
+
are left untouched.
|
|
62
78
|
|
|
63
79
|
Beyond project mounts, a container can mount named volumes (prefixed
|
|
64
80
|
`DSH_PODMAN_VOLUME_PREFIX`, default `dsh-podman-`, and auto-created by podman on
|
|
@@ -101,6 +117,13 @@ and writes files. Each guest container gets exactly one socket directory
|
|
|
101
117
|
bind-mounted into it, so a guest never sees the orchestrator's control socket
|
|
102
118
|
nor any other workspace's socket directory.
|
|
103
119
|
|
|
120
|
+
The guest API exposes no watcher, so the plugin's `ctx.fs` provider reports
|
|
121
|
+
`FS_IO_ERROR` (`Filesystem watching is not supported by this provider.`) for
|
|
122
|
+
`watch`. The harness's workspace-file change stream therefore answers
|
|
123
|
+
`workspace-file/watch-unsupported`, and the browser's file tree refreshes on
|
|
124
|
+
demand instead of live — the same stance the harness's SSH filesystem provider
|
|
125
|
+
takes for a remote path.
|
|
126
|
+
|
|
104
127
|
Commands and daemons inherit the guest agent's environment, minus the reserved
|
|
105
128
|
`DSH_PODMAN` namespace: the agent's own token stays with the agent instead of
|
|
106
129
|
being copied into everything it starts. A daemon started with `inheritEnv=false`
|
|
@@ -145,20 +168,22 @@ Containerfile, so a build writes nothing to the orchestrator's state directory.
|
|
|
145
168
|
from a public registry per `DSH_PODMAN_BASE_IMAGE_PREFIX`), then rebuilds the
|
|
146
169
|
stored custom images **in dependency order** — each parent before the images
|
|
147
170
|
derived from it. An image whose rebuild fails, and every image that depends on
|
|
148
|
-
it, is reported in `skipped` while the rest continue.
|
|
171
|
+
it, is reported in `skipped` while the rest continue. A base image the call
|
|
172
|
+
could not build or pull is reported there too, by its short name; `rebuilt`
|
|
173
|
+
lists only the custom images, so it never names a base the call had to ensure.
|
|
149
174
|
|
|
150
175
|
When a host cache is configured (`DSH_PODMAN_HOST_*_CACHE`, mounted into both
|
|
151
176
|
the build container and the orchestrator), builds reuse downloaded packages. The
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
177
|
+
Podman page reports each cache's size and can clean it — keep the newest version
|
|
178
|
+
of every package, or empty the cache. The builder serializes a cleanup against
|
|
179
|
+
builds with a read/write lock, so a cleanup never deletes a package out from
|
|
180
|
+
under a running build.
|
|
156
181
|
|
|
157
|
-
##
|
|
182
|
+
## Podman page transport
|
|
158
183
|
|
|
159
|
-
The
|
|
160
|
-
API path (`/api/podman/card`), registered on the connection service so
|
|
161
|
-
carrier applies its Host/Origin fence and browser authentication first:
|
|
184
|
+
The Podman page talks to the host over one authenticated fetch route below the
|
|
185
|
+
harness API path (`/api/podman/card`), registered on the connection service so
|
|
186
|
+
the carrier applies its Host/Origin fence and browser authentication first:
|
|
162
187
|
|
|
163
188
|
- `GET` returns the live orchestrator snapshot (containers, images, workspaces,
|
|
164
189
|
volumes, secrets, caches), built fresh on every request.
|
|
@@ -166,8 +191,34 @@ carrier applies its Host/Origin fence and browser authentication first:
|
|
|
166
191
|
`image_rebuild` / `image_rebuild_all` / volume / secret / secret-env / mount
|
|
167
192
|
ops) against the orchestrator and returns the notice to show.
|
|
168
193
|
|
|
169
|
-
The
|
|
170
|
-
image, the
|
|
171
|
-
can render approval text in the session language — see
|
|
172
|
-
[Approval](usage.md#approval)).
|
|
173
|
-
|
|
194
|
+
The plugin's own config therefore holds **only real preferences**: the default
|
|
195
|
+
image, the default environment, and the page's active locale (`uiLocale`, so the
|
|
196
|
+
host can render approval text in the session language — see
|
|
197
|
+
[Approval](usage.md#approval)). They are volatile fields, so an edit applies
|
|
198
|
+
without reloading the plugin. Nothing derived from the orchestrator is
|
|
199
|
+
persisted, and no command round-trips through the config document.
|
|
200
|
+
|
|
201
|
+
## Podman terminal transport
|
|
202
|
+
|
|
203
|
+
The Podman terminal tab talks to guest ptys through its own authenticated routes
|
|
204
|
+
on the same connection service (`/api/podman/terminal`,
|
|
205
|
+
`/api/podman/terminal/shells`, `/api/podman/terminal/retained`). The open route
|
|
206
|
+
streams newline-delimited JSON frames (ready, snapshot, base64 output, title,
|
|
207
|
+
exit, error, detached) and takes control requests (input, resize, rename, close)
|
|
208
|
+
over a POST; the carrier applies the same Host/Origin fence and browser
|
|
209
|
+
authentication as the card route.
|
|
210
|
+
|
|
211
|
+
Each terminal is keyed by `(sessionId, tabId)` and retained by the host: the
|
|
212
|
+
guest pty stays alive while no browser is attached, and every chunk is also fed
|
|
213
|
+
to a headless terminal emulator whose serialized screen is replayed on reattach,
|
|
214
|
+
so a reload keeps the shell and its scrollback. Shell discovery runs inside the
|
|
215
|
+
target container: candidate names are resolved with POSIX `command -v` on the
|
|
216
|
+
container's PATH (including the deployment's PATH additions) and merged with
|
|
217
|
+
`/etc/shells` and `$SHELL`, so only shells the container really provides are
|
|
218
|
+
offered, most capable first — the order mirrors the harness's own candidate
|
|
219
|
+
preference and ends with the minimal POSIX shells, and the first entry is the
|
|
220
|
+
client's default. The tab's workspace is resolved by the host from the session's
|
|
221
|
+
own working directory — never chosen, shown or defaulted in the browser: a
|
|
222
|
+
session outside every workspace is reported, and the client only uses the
|
|
223
|
+
project name and workspace slug it is given. The selected path is re-verified
|
|
224
|
+
before the shell starts.
|
package/docs/architecture.zh.md
CHANGED
|
@@ -23,14 +23,24 @@ dsh-podman 由三个组件组成:
|
|
|
23
23
|
orchestrator(编排器)并通信 gRPC。请求携带共享
|
|
24
24
|
token(`DSH_PODMAN_ORCHESTRATOR_TOKEN`)作为 `authorization: bearer` 标头。
|
|
25
25
|
|
|
26
|
+
请求还会在 `x-dsh-podman-plugin-version` 标头中携带插件版本。插件与 orchestrator
|
|
27
|
+
分别部署(前者是 dsh 镜像中的 npm 包,后者是此二进制文件),因此 orchestrator
|
|
28
|
+
会拒绝来自**主版本**不同的插件的控制调用,并指出两者的版本以及哪一方更旧——不同步的部署会明确失败,而不会被误读。缺失或无法解析的版本同样会被拒绝。次版本或修订版本不同是兼容的:调用照常进行,orchestrator
|
|
29
|
+
会按插件版本记录一次警告,Podman 页面也会显示。`GetVersion`
|
|
30
|
+
不受该检查约束,因此插件始终可以获知 orchestrator 的版本。
|
|
31
|
+
|
|
26
32
|
orchestrator 服务公开以下 gRPC 方法(同时支撑 UI
|
|
27
33
|
和工具):`ListContainers`(仅返回 orchestrator 创建的 guest
|
|
28
|
-
容器——它不拥有的容器永远不会被暴露)、`
|
|
34
|
+
容器——它不拥有的容器永远不会被暴露)、`EnsureContainer{workspace_slug, container}`(唯一的接入路径:返回指定容器,若其缺失、已停止或运行过期的
|
|
35
|
+
guest
|
|
36
|
+
agent,则先重建它)、`StartContainer{workspace_slug, container, image_id, mounts, env, secret_env, paths}`(在工作区的
|
|
29
37
|
pod 中创建或替换容器;空的 `container`
|
|
30
38
|
指向默认容器,其他名称会被校验)、`RecreateContainer{workspace_slug, container, image_id, mounts, env, paths}`(停止、删除并重新创建容器,可选择使用新镜像、项目挂载、环境或
|
|
31
39
|
PATH 附加项;空的 `image_id`
|
|
32
|
-
保留工作区当前的镜像)、`RemoveContainer`、`AddContainerMount`、`RemoveContainerMount`、`SetContainerPaths`、`AddContainerSecret`
|
|
33
|
-
和 `RemoveContainerSecret
|
|
40
|
+
保留工作区当前的镜像)、`RemoveContainer`、`AddContainerMount`、`UpdateContainerMount`、`RemoveContainerMount`、`SetContainerPaths`、`AddContainerSecret`
|
|
41
|
+
和 `RemoveContainerSecret`。该服务还公开支撑 Podman
|
|
42
|
+
页面的工作区、卷、机密、镜像(构建、重建、删除以及拉取基础镜像)和缓存(列出与清理)操作,以及
|
|
43
|
+
`GetVersion`。
|
|
34
44
|
|
|
35
45
|
## 工作区与 pod
|
|
36
46
|
|
|
@@ -41,10 +51,12 @@ UUID),因此其容器共享一个网络命名空间。每个工作区都有
|
|
|
41
51
|
和 `/run` 上是 podman 的可写 tmpfs(`/dev` 和 `/dev/shm`
|
|
42
52
|
保持可写)。所有其他可写状态都存在于项目绑定挂载、命名卷或 tmpfs 挂载中。guest
|
|
43
53
|
文件 API 可以访问 `/tmp`,而 guest agent 会把超出上限的命令输出写入
|
|
44
|
-
`/tmp/dsh-podman`
|
|
54
|
+
`/tmp/dsh-podman` 下的溢出文件。过大的工具结果也会由插件的 `ctx.spillStore`
|
|
55
|
+
溢出写入同一位置下的
|
|
56
|
+
`/tmp/dsh-podman/spill/<session>/`——因此模型会用容器文件工具读回该产物,而不是使用容器无法访问的主机路径。
|
|
45
57
|
|
|
46
58
|
工作区的 Pod 会在其最后一个容器被移除时拆除,也可直接通过
|
|
47
|
-
`RemoveWorkspace
|
|
59
|
+
`RemoveWorkspace`(Podman 页面中的 **移除 Pod**
|
|
48
60
|
操作)拆除:它会停止各容器的守护进程、移除 Pod 及其容器、清理它们的 socket
|
|
49
61
|
目录,并删除存储的工作区记录。卷、机密和项目数据保持不变。
|
|
50
62
|
|
|
@@ -60,6 +72,16 @@ UUID),因此其容器共享一个网络命名空间。每个工作区都有
|
|
|
60
72
|
|
|
61
73
|
包含保留路径的目标会被拒绝,位于其内部的目标同样会被拒绝,因为它会隐藏其下的所有内容。
|
|
62
74
|
|
|
75
|
+
通过 guest exec API 运行的命令,除非调用方明确要求管道,否则它们的 stdin 是
|
|
76
|
+
`/dev/null`,因此读取 stdin 的命令会看到 EOF 而不会阻塞(因此不带路径的裸
|
|
77
|
+
`rg`/`grep` 会搜索工作目录,而不是读取空管道)。`container_glob` 和
|
|
78
|
+
`container_grep` 还会把解析后的路径作为搜索目录传给 ripgrep,而绝不会作为
|
|
79
|
+
`--glob` 模式(包含 `/` 的路径永远无法匹配这种模式),并且 ripgrep 失败(退出码
|
|
80
|
+
2)会被报告为工具错误,而不是返回空结果。ripgrep 会把包含 `/` 的 `--glob`
|
|
81
|
+
模式锚定到进程工作目录,因此当 harness
|
|
82
|
+
启动的目录列举以绝对搜索根目录运行时,它会从该根目录运行:此时 `glob` 的
|
|
83
|
+
`pattern` 锚定到其 `path`,而打印出的路径保持绝对。
|
|
84
|
+
|
|
63
85
|
项目挂载会解析符号链接并被限制在 projects root
|
|
64
86
|
内,因此可写项目中的符号链接不能将绑定挂载重定向到其外部的路径。带有绝对目标的符号链接永远不会被跟随;请直接命名另一个项目。
|
|
65
87
|
|
|
@@ -71,6 +93,12 @@ API,运行并监督后台**守护进程**,并读取和写入文件。每个
|
|
|
71
93
|
容器恰好获得一个绑定挂载到其中的套接字目录,因此 guest 永远不会看到 orchestrator
|
|
72
94
|
的 control socket,也不会看到任何其他工作区的套接字目录。
|
|
73
95
|
|
|
96
|
+
Guest API 不提供监视器,因此插件的 `ctx.fs` 提供者对 `watch` 返回
|
|
97
|
+
`FS_IO_ERROR`(`Filesystem watching is not supported by this provider.`)。harness
|
|
98
|
+
的工作区文件变更流因此返回
|
|
99
|
+
`workspace-file/watch-unsupported`,浏览器的文件树改为按需刷新,而非实时刷新——与
|
|
100
|
+
harness 自带的 SSH 文件系统提供者对远程路径采取的做法一致。
|
|
101
|
+
|
|
74
102
|
命令和守护进程继承 guest agent 的环境,减去保留的 `DSH_PODMAN` 命名空间:agent
|
|
75
103
|
自身的 token 保留在 agent 中,而不会被复制到它所启动的每个东西中。以
|
|
76
104
|
`inheritEnv=false` 启动的守护进程则只接收 `PATH`/`HOME` 基线以及自身的 `env`。
|
|
@@ -107,14 +135,17 @@ API,因此构建不会向 orchestrator 的状态目录写入任何内容。
|
|
|
107
135
|
|
|
108
136
|
`image_rebuild_all` 首先确保每个基础镜像(根据 `DSH_PODMAN_BASE_IMAGE_PREFIX`
|
|
109
137
|
在本地构建或从公共注册表拉取),然后**按依赖顺序**重建存储的自定义镜像——每个父镜像都在从它派生的镜像之前。重建失败的镜像以及依赖它的每个镜像都会被报告在
|
|
110
|
-
`skipped`
|
|
138
|
+
`skipped`
|
|
139
|
+
中,其余镜像继续。本次调用无法构建或拉取的基础镜像也会以短名称报告在那里;`rebuilt`
|
|
140
|
+
只列出自定义镜像,因此不会包含本次调用所确保的基础镜像。
|
|
111
141
|
|
|
112
142
|
配置了主机缓存时(`DSH_PODMAN_HOST_*_CACHE`,同时挂载到构建容器和 orchestrator
|
|
113
|
-
|
|
143
|
+
中),构建会复用已下载的软件包。Podman
|
|
144
|
+
页面会报告每个缓存的大小并可清理它——保留每个软件包的最新版本,或清空缓存。构建器用读写锁将清理与构建串行化,因此清理绝不会在构建进行时删除其正在使用的软件包。
|
|
114
145
|
|
|
115
|
-
##
|
|
146
|
+
## Podman 页面传输
|
|
116
147
|
|
|
117
|
-
|
|
148
|
+
Podman 页面通过 harness API 路径(`/api/podman/card`)之下的一个已认证 fetch
|
|
118
149
|
路由与主机通信;该路由注册在 connection 服务上,因此承载层会先应用其 Host/Origin
|
|
119
150
|
校验和浏览器认证:
|
|
120
151
|
|
|
@@ -124,6 +155,25 @@ API,因此构建不会向 orchestrator 的状态目录写入任何内容。
|
|
|
124
155
|
`image_rebuild` / `image_rebuild_all` / volume / secret / secret-env / mount
|
|
125
156
|
操作),并返回要显示的提示。
|
|
126
157
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
158
|
+
因此插件自身的配置中**只保留真正的偏好**:默认镜像、默认环境变量,以及 Podman
|
|
159
|
+
页面的当前语言(`uiLocale`,以便主机以会话语言呈现审批文本——参见[审批](usage.zh.md#审批))。它们是
|
|
160
|
+
volatile 字段,修改后无需重新加载插件即可生效。orchestrator
|
|
161
|
+
派生的任何内容都不会被持久化,也不会有命令经由配置文档往返。
|
|
162
|
+
|
|
163
|
+
## Podman 终端传输
|
|
164
|
+
|
|
165
|
+
Podman 终端标签页通过自身在同一 connection
|
|
166
|
+
服务上的已认证路由(`/api/podman/terminal`、`/api/podman/terminal/shells`、`/api/podman/terminal/retained`)与
|
|
167
|
+
guest pty 通信。打开路由以换行分隔的 JSON 帧(ready、snapshot、base64
|
|
168
|
+
输出、title、exit、error、detached)进行流式传输,并通过 POST
|
|
169
|
+
接收控制请求(input、resize、rename、close);承载层会像卡片路由一样应用
|
|
170
|
+
Host/Origin 校验和浏览器认证。
|
|
171
|
+
|
|
172
|
+
每个终端以 `(sessionId, tabId)` 为键并由主机保留:没有浏览器接入时 guest pty
|
|
173
|
+
仍然存活,同时每个输出块都会写入一个无头终端模拟器,重新接入时回放其序列化屏幕,因此刷新页面不会丢失
|
|
174
|
+
shell 及其回滚缓冲。shell 探测在目标容器内进行:以 POSIX `command -v` 在该容器的
|
|
175
|
+
PATH(含部署的 PATH 追加项)中解析候选名称,并与 `/etc/shells` 和 `$SHELL`
|
|
176
|
+
合并,因此只提供容器确实拥有的 shell;顺序按能力从强到弱(与 harness
|
|
177
|
+
自身的候选顺序一致,最精简的 POSIX shell
|
|
178
|
+
排在最后),第一项即客户端的默认值。标签页的工作区由主机根据会话自身的工作目录解析,浏览器既不可选择、不会显示,也不会回退:会话不在任何工作区内时会直接提示,客户端只使用主机给出的项目名与工作区
|
|
179
|
+
slug。所选路径在启动前会再次校验。
|