socketduct 0.1.24 → 0.1.26
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/AGENTS.md +2 -2
- package/changelog.d/20261010130509-bound-log-frame-compression-memory.md +1 -0
- package/docs/runbooks/jsonl-integrity-rollout.md +8 -6
- package/package.json +2 -2
- package/scripts/test.js +9 -14
- package/src/client/multiplexed-transport-set.js +20 -2
- package/src/logging/compressed-log-writer.js +1 -1
- package/src/logging/zstd-frame-codec.js +15 -1
- package/src/reverse/client.js +44 -1
- package/src/reverse/hub.js +10 -1
- package/src/reverse/runtime.js +11 -2
- package/test/e2e/gateway-process.test.js +93 -92
- package/test/e2e/logger-shutdown-process.test.js +40 -37
- package/test/e2e/reverse-client-recovery-process.test.js +107 -103
- package/test/e2e/reverse-gateway-process.test.js +328 -318
- package/test/integration/initial-target-recovery.test.js +333 -334
- package/test/integration/logging-cli.test.js +121 -119
- package/test/integration/logging-compressed-recovery.test.js +96 -95
- package/test/integration/multiplexed-compatibility.test.js +178 -177
- package/test/integration/multiplexed-corrections.test.js +360 -359
- package/test/integration/multiplexed-failures.test.js +253 -251
- package/test/integration/multiplexed-transport-set.test.js +334 -322
- package/test/integration/multiplexed-transport.test.js +135 -134
- package/test/integration/reverse-gateway-stream-id-reset.test.js +90 -89
- package/test/integration/reverse-gateway.test.js +2309 -2218
- package/test/integration/reverse-resume-continuity.test.js +219 -218
- package/test/integration/reverse-route-class.test.js +363 -278
- package/test/integration/socketduct-http-agent.test.js +314 -312
- package/test/integration/socketduct-https-agent.test.js +516 -485
- package/test/integration/stream-compression.test.js +862 -824
- package/test/integration/tcp-forwarding.test.js +2550 -2447
- package/test/integration/tls-forwarding.test.js +593 -592
- package/test/unit/auth/auth.test.js +43 -44
- package/test/unit/auth/static-token-store.test.js +9 -8
- package/test/unit/client/connect.test.js +41 -40
- package/test/unit/client/deferred-virtual-socket.test.js +95 -94
- package/test/unit/client/multiplexed-spool-budget.test.js +155 -154
- package/test/unit/client/multiplexed-transport-set.test.js +845 -732
- package/test/unit/client/socketduct-socket.test.js +768 -771
- package/test/unit/client/virtual-socket.test.js +198 -168
- package/test/unit/config/docker-gateway-files.test.js +224 -224
- package/test/unit/config/environment.test.js +472 -492
- package/test/unit/index.test.js +53 -52
- package/test/unit/logging/compressed-log-worker.test.js +23 -22
- package/test/unit/logging/compressed-log-writer.test.js +155 -154
- package/test/unit/logging/log-retention.test.js +52 -47
- package/test/unit/logging/log-segment-index.test.js +84 -81
- package/test/unit/logging/zstd-frame-codec.test.js +28 -19
- package/test/unit/protocol/compression.test.js +254 -298
- package/test/unit/protocol/frame.test.js +280 -276
- package/test/unit/protocol/v3-compression.test.js +21 -23
- package/test/unit/protocol/v3-frame.test.js +42 -41
- package/test/unit/release-configuration.test.js +15 -12
- package/test/unit/reverse/disconnect-attribution.test.js +65 -64
- package/test/unit/reverse-terminal-outcomes.test.js +53 -50
- package/test/unit/server/control-handler.test.js +232 -231
- package/test/unit/server/primary.test.js +51 -49
- package/test/unit/server/relay-runtime.test.js +280 -294
- package/test/unit/sessions/session.test.js +48 -62
- package/test/unit/spool/spool-file.test.js +401 -385
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {describe, expect, it} from "@velocious/testing"
|
|
2
2
|
import {spawnSync} from "node:child_process"
|
|
3
3
|
import {copyFile, mkdtemp, readFile, readdir, rm} from "node:fs/promises"
|
|
4
4
|
import {tmpdir} from "node:os"
|
|
5
5
|
import {delimiter, join} from "node:path"
|
|
6
|
-
import test from "node:test"
|
|
7
6
|
|
|
8
7
|
const composePath = new URL("../../../docker-compose.yml", import.meta.url)
|
|
9
8
|
const developmentComposePath = new URL("../../../docker-compose.dev.yml", import.meta.url)
|
|
@@ -27,252 +26,253 @@ async function githubWorkflowNames() {
|
|
|
27
26
|
}
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
describe("docker gateway files", () => {
|
|
30
|
+
it("TensorBuzz exclusively owns verification and container build validation", async () => {
|
|
31
|
+
const [packageJson, tensorbuzz, workflowNames] = await Promise.all([
|
|
32
|
+
readFile(packagePath, "utf8"),
|
|
33
|
+
readFile(tensorbuzzPath, "utf8"),
|
|
34
|
+
githubWorkflowNames()
|
|
35
|
+
])
|
|
36
|
+
const packageMetadata = JSON.parse(packageJson)
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
test("Docker gateway files define a private Docker socket shim target", async () => {
|
|
53
|
-
const [compose, envExample, docs] = await Promise.all([
|
|
54
|
-
readFile(composePath, "utf8"),
|
|
55
|
-
readFile(envExamplePath, "utf8"),
|
|
56
|
-
readFile(docsPath, "utf8")
|
|
57
|
-
])
|
|
58
|
-
|
|
59
|
-
assert.match(compose, /docker-socket-shim:/)
|
|
60
|
-
assert.match(compose, /profiles:\n\s+- docker-host/)
|
|
61
|
-
assert.match(compose, /\/var\/run\/docker\.sock:\/var\/run\/docker\.sock/)
|
|
62
|
-
assert.match(compose, /expose:\n\s+- "2375"/)
|
|
63
|
-
assert.doesNotMatch(compose, /2375:2375/)
|
|
64
|
-
assert.match(envExample, /SOCKETDUCT_TARGET_ALLOWLIST=docker-socket-shim:2375/)
|
|
65
|
-
assert.match(docs, /Docker socket shim/)
|
|
66
|
-
})
|
|
38
|
+
expect(workflowNames).toEqual([])
|
|
39
|
+
expect(tensorbuzz).toMatch(/^dependency_roots:\n {2}- path: \.$/m)
|
|
40
|
+
expect(tensorbuzz).not.toMatch(/^\s*- npm ci$/m)
|
|
41
|
+
expect(tensorbuzz).toMatch(/DOCKER_HOST: "tcp:\/\/docker_server:2375"/)
|
|
42
|
+
expect(tensorbuzz).toMatch(/docker_server:\n\s+image: "docker:[^"]+-dind"/)
|
|
43
|
+
expect(tensorbuzz).toMatch(/privileged: true/)
|
|
44
|
+
expect(tensorbuzz).toMatch(/docker build --tag socketduct-ci \./)
|
|
45
|
+
expect(tensorbuzz).not.toMatch(/docker (?:login|push)/)
|
|
46
|
+
// release-patch refuses to publish a package marked "private": true, so the
|
|
47
|
+
// published UNLICENSED npm release requires the flag to be absent or false.
|
|
48
|
+
expect(packageMetadata.private).not.toBe(true)
|
|
49
|
+
expect(packageMetadata.license).toBe("UNLICENSED")
|
|
50
|
+
})
|
|
67
51
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
assert.doesNotMatch(compose, /\bports:/)
|
|
75
|
-
assert.doesNotMatch(compose, /socat|socket-shim|2375:2375/)
|
|
76
|
-
})
|
|
52
|
+
it("Docker gateway files define a private Docker socket shim target", async () => {
|
|
53
|
+
const [compose, envExample, docs] = await Promise.all([
|
|
54
|
+
readFile(composePath, "utf8"),
|
|
55
|
+
readFile(envExamplePath, "utf8"),
|
|
56
|
+
readFile(docsPath, "utf8")
|
|
57
|
+
])
|
|
77
58
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
59
|
+
expect(compose).toMatch(/docker-socket-shim:/)
|
|
60
|
+
expect(compose).toMatch(/profiles:\n\s+- docker-host/)
|
|
61
|
+
expect(compose).toMatch(/\/var\/run\/docker\.sock:\/var\/run\/docker\.sock/)
|
|
62
|
+
expect(compose).toMatch(/expose:\n\s+- "2375"/)
|
|
63
|
+
expect(compose).not.toMatch(/2375:2375/)
|
|
64
|
+
expect(envExample).toMatch(/SOCKETDUCT_TARGET_ALLOWLIST=docker-socket-shim:2375/)
|
|
65
|
+
expect(docs).toMatch(/Docker socket shim/)
|
|
66
|
+
})
|
|
85
67
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
68
|
+
it("native reverse gateway Compose has outbound-only private Docker access", async () => {
|
|
69
|
+
const compose = await readFile(reverseComposePath, "utf8")
|
|
70
|
+
expect(compose).toMatch(/SOCKETDUCT_MODE: reverse-gateway/)
|
|
71
|
+
expect(compose).toMatch(/SOCKETDUCT_TARGET_ALLOWLIST: docker-server:2375/)
|
|
72
|
+
expect(compose).toMatch(/docker-control:/)
|
|
73
|
+
expect(compose).toMatch(/wan-egress:/)
|
|
74
|
+
expect(compose).not.toMatch(/\bports:/)
|
|
75
|
+
expect(compose).not.toMatch(/socat|socket-shim|2375:2375/)
|
|
76
|
+
})
|
|
94
77
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
78
|
+
it("production Compose roles expose the opt-in persistent log mount contract", async () => {
|
|
79
|
+
const [compose, reverseCompose, envExample, docs] = await Promise.all([
|
|
80
|
+
readFile(composePath, "utf8"),
|
|
81
|
+
readFile(reverseComposePath, "utf8"),
|
|
82
|
+
readFile(envExamplePath, "utf8"),
|
|
83
|
+
readFile(docsPath, "utf8")
|
|
84
|
+
])
|
|
101
85
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
86
|
+
for (const model of [compose, reverseCompose]) {
|
|
87
|
+
expect(model).toMatch(/\$\{SOCKETDUCT_LOG_HOST_DIRECTORY:-\.\/logs\}:\/var\/log\/socketduct/)
|
|
88
|
+
}
|
|
89
|
+
expect(envExample).toMatch(/SOCKETDUCT_LOG_DIRECTORY=\/var\/log\/socketduct/)
|
|
90
|
+
expect(envExample).toMatch(/SOCKETDUCT_LOG_HOST_DIRECTORY=\/srv\/socketduct\/logs/)
|
|
91
|
+
expect(docs).toMatch(/SOCKETDUCT_LOG_HOST_DIRECTORY/)
|
|
92
|
+
expect(docs).toMatch(/\/var\/log\/socketduct/)
|
|
93
|
+
})
|
|
108
94
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
95
|
+
it("production gateway Compose requires an immutable Socketduct GHCR image", async () => {
|
|
96
|
+
const [compose, developmentCompose, envExample] = await Promise.all([
|
|
97
|
+
readFile(composePath, "utf8"),
|
|
98
|
+
readFile(developmentComposePath, "utf8"),
|
|
99
|
+
readFile(envExamplePath, "utf8")
|
|
100
|
+
])
|
|
113
101
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
"ghcr.io/kaspernj/socketduct:40cc261",
|
|
121
|
-
`ghcr.io/kaspernj/socketduct:release@sha256:${"a".repeat(64)}`,
|
|
122
|
-
`ghcr.io/kaspernj/socketduct@sha256:${"A".repeat(64)}`
|
|
123
|
-
]) {
|
|
124
|
-
await assert.rejects(
|
|
125
|
-
deployGateway({image, run: (command, args) => { calls.push([command, args]); return "" }}),
|
|
126
|
-
/immutable GHCR image/
|
|
127
|
-
)
|
|
128
|
-
}
|
|
129
|
-
assert.deepEqual(calls, [])
|
|
130
|
-
})
|
|
102
|
+
expect(compose).toMatch(/image: \$\{SOCKETDUCT_IMAGE:\?[^}]+\}/)
|
|
103
|
+
expect(compose).not.toMatch(/\bbuild:/)
|
|
104
|
+
expect(compose).not.toMatch(/socketduct:latest/)
|
|
105
|
+
expect(developmentCompose).toMatch(/\bbuild:/)
|
|
106
|
+
expect(envExample).toMatch(/SOCKETDUCT_IMAGE=ghcr\.io\/kaspernj\/socketduct@sha256:/)
|
|
107
|
+
})
|
|
131
108
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const digest = "a".repeat(64)
|
|
137
|
-
const image = `ghcr.io/kaspernj/socketduct@sha256:${digest}`
|
|
138
|
-
const environment = {
|
|
139
|
-
PATH: "/usr/bin",
|
|
140
|
-
DOCKER_HOST: "ssh://gateway.example.test",
|
|
141
|
-
DOCKER_CONTEXT: "production-gateway",
|
|
142
|
-
COMPOSE_FILE: "/tmp/mutable-compose.yml",
|
|
143
|
-
COMPOSE_ENV_FILES: "/tmp/mutable.env",
|
|
144
|
-
COMPOSE_DISABLE_ENV_FILE: "1",
|
|
145
|
-
COMPOSE_IGNORE_ORPHANS: "1",
|
|
146
|
-
COMPOSE_REMOVE_ORPHANS: "1",
|
|
147
|
-
COMPOSE_PATH_SEPARATOR: ";",
|
|
148
|
-
COMPOSE_PROFILES: "docker-host",
|
|
149
|
-
COMPOSE_PROJECT_NAME: "ambient-project"
|
|
150
|
-
}
|
|
109
|
+
it("gateway deployment rejects mutable images before running Docker", async () => {
|
|
110
|
+
const {deployGateway} = await import("../../../scripts/deploy-gateway.js")
|
|
111
|
+
/** @type {Array<[string, string[]]>} */
|
|
112
|
+
const calls = []
|
|
151
113
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
114
|
+
for (const image of [
|
|
115
|
+
undefined,
|
|
116
|
+
"",
|
|
117
|
+
"socketduct:latest",
|
|
118
|
+
"ghcr.io/kaspernj/socketduct:latest",
|
|
119
|
+
"ghcr.io/kaspernj/socketduct:main",
|
|
120
|
+
"ghcr.io/kaspernj/socketduct:40cc261",
|
|
121
|
+
`ghcr.io/kaspernj/socketduct:release@sha256:${"a".repeat(64)}`,
|
|
122
|
+
`ghcr.io/kaspernj/socketduct@sha256:${"A".repeat(64)}`
|
|
123
|
+
]) {
|
|
124
|
+
await expect(
|
|
125
|
+
deployGateway({image, run: (command, args) => { calls.push([command, args]); return "" }})
|
|
126
|
+
).rejects.toThrow(/immutable GHCR image/)
|
|
161
127
|
}
|
|
128
|
+
expect(calls).toEqual([])
|
|
162
129
|
})
|
|
163
130
|
|
|
164
|
-
|
|
165
|
-
"
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
assert.equal(options.env.COMPOSE_IGNORE_ORPHANS, "false")
|
|
184
|
-
assert.equal(options.env.COMPOSE_REMOVE_ORPHANS, "false")
|
|
185
|
-
assert.equal(options.env.COMPOSE_PROFILES, "")
|
|
186
|
-
assert.equal(options.env.COMPOSE_PROJECT_NAME, "socketduct-gateway")
|
|
187
|
-
assert.equal(options.env.SOCKETDUCT_IMAGE, image)
|
|
188
|
-
}
|
|
189
|
-
})
|
|
131
|
+
it("gateway deployment fixes Compose inputs, neutralizes ambient selectors, then verifies and applies", async () => {
|
|
132
|
+
const {deployGateway} = await import("../../../scripts/deploy-gateway.js")
|
|
133
|
+
/** @type {Array<[string, string[], {captureStdout?: boolean, env: Record<string, string | undefined>}]>} */
|
|
134
|
+
const calls = []
|
|
135
|
+
const digest = "a".repeat(64)
|
|
136
|
+
const image = `ghcr.io/kaspernj/socketduct@sha256:${digest}`
|
|
137
|
+
const environment = {
|
|
138
|
+
PATH: "/usr/bin",
|
|
139
|
+
DOCKER_HOST: "ssh://gateway.example.test",
|
|
140
|
+
DOCKER_CONTEXT: "production-gateway",
|
|
141
|
+
COMPOSE_FILE: "/tmp/mutable-compose.yml",
|
|
142
|
+
COMPOSE_ENV_FILES: "/tmp/mutable.env",
|
|
143
|
+
COMPOSE_DISABLE_ENV_FILE: "1",
|
|
144
|
+
COMPOSE_IGNORE_ORPHANS: "1",
|
|
145
|
+
COMPOSE_REMOVE_ORPHANS: "1",
|
|
146
|
+
COMPOSE_PATH_SEPARATOR: ";",
|
|
147
|
+
COMPOSE_PROFILES: "docker-host",
|
|
148
|
+
COMPOSE_PROJECT_NAME: "ambient-project"
|
|
149
|
+
}
|
|
190
150
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
151
|
+
await deployGateway({
|
|
152
|
+
environment,
|
|
153
|
+
image,
|
|
154
|
+
projectDirectory: "/srv/socketduct release #1",
|
|
155
|
+
run: (command, args, options) => {
|
|
156
|
+
calls.push([command, args, options])
|
|
157
|
+
return args.includes("config")
|
|
158
|
+
? JSON.stringify({services: {"socketduct-relay": {image}}})
|
|
159
|
+
: ""
|
|
160
|
+
}
|
|
161
|
+
})
|
|
196
162
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
163
|
+
const compose = [
|
|
164
|
+
"compose",
|
|
165
|
+
"--file", "/srv/socketduct release #1/docker-compose.yml",
|
|
166
|
+
"--env-file", "/srv/socketduct release #1/.env",
|
|
167
|
+
"--project-directory", "/srv/socketduct release #1",
|
|
168
|
+
"--project-name", "socketduct-gateway"
|
|
169
|
+
]
|
|
170
|
+
expect(calls.map(([command, args, options]) => [command, args, options.captureStdout])).toEqual([
|
|
171
|
+
["docker", [...compose, "config", "--format", "json"], true],
|
|
172
|
+
["docker", [...compose, "pull"], undefined],
|
|
173
|
+
["docker", [...compose, "up", "-d", "--wait", "--no-build"], undefined]
|
|
174
|
+
])
|
|
175
|
+
for (const [, , options] of calls) {
|
|
176
|
+
expect(options.env.DOCKER_HOST).toBe(environment.DOCKER_HOST)
|
|
177
|
+
expect(options.env.DOCKER_CONTEXT).toBe(environment.DOCKER_CONTEXT)
|
|
178
|
+
expect(options.env.COMPOSE_FILE).toBe("/srv/socketduct release #1/docker-compose.yml")
|
|
179
|
+
expect(options.env.COMPOSE_ENV_FILES).toBe("/srv/socketduct release #1/.env")
|
|
180
|
+
expect(options.env.COMPOSE_PATH_SEPARATOR).toBe(delimiter)
|
|
181
|
+
expect(options.env.COMPOSE_DISABLE_ENV_FILE).toBe("false")
|
|
182
|
+
expect(options.env.COMPOSE_IGNORE_ORPHANS).toBe("false")
|
|
183
|
+
expect(options.env.COMPOSE_REMOVE_ORPHANS).toBe("false")
|
|
184
|
+
expect(options.env.COMPOSE_PROFILES).toBe("")
|
|
185
|
+
expect(options.env.COMPOSE_PROJECT_NAME).toBe("socketduct-gateway")
|
|
186
|
+
expect(options.env.SOCKETDUCT_IMAGE).toBe(image)
|
|
207
187
|
}
|
|
208
188
|
})
|
|
209
189
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
test("gateway deployment fails closed on a rendered image mismatch or build source", async () => {
|
|
219
|
-
const {deployGateway} = await import("../../../scripts/deploy-gateway.js")
|
|
220
|
-
const image = `ghcr.io/kaspernj/socketduct@sha256:${"c".repeat(64)}`
|
|
221
|
-
const renderedModels = [
|
|
222
|
-
{services: {"socketduct-relay": {image: "ghcr.io/kaspernj/socketduct:latest"}}},
|
|
223
|
-
{services: {"socketduct-relay": {build: {context: "."}, image}}},
|
|
224
|
-
{services: {}}
|
|
225
|
-
]
|
|
190
|
+
it("gateway deployment enables only the explicit docker-host profile", async () => {
|
|
191
|
+
const {deployGateway} = await import("../../../scripts/deploy-gateway.js")
|
|
192
|
+
/** @type {Array<{args: string[], environment: Record<string, string | undefined>}>} */
|
|
193
|
+
const calls = []
|
|
194
|
+
const image = `ghcr.io/kaspernj/socketduct@sha256:${"b".repeat(64)}`
|
|
226
195
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
environment: {},
|
|
196
|
+
await deployGateway({
|
|
197
|
+
dockerHost: true,
|
|
198
|
+
environment: {COMPOSE_PROFILES: "unexpected"},
|
|
231
199
|
image,
|
|
232
200
|
projectDirectory: "/srv/socketduct",
|
|
233
|
-
run: () => {
|
|
234
|
-
calls
|
|
235
|
-
return
|
|
201
|
+
run: (_command, args, options) => {
|
|
202
|
+
calls.push({args, environment: options.env})
|
|
203
|
+
return args.includes("config")
|
|
204
|
+
? JSON.stringify({services: {"socketduct-relay": {image}}})
|
|
205
|
+
: ""
|
|
236
206
|
}
|
|
237
|
-
}), /rendered.*socketduct-relay|build source/i)
|
|
238
|
-
assert.equal(calls, 1)
|
|
239
|
-
}
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
test("gateway deployment CLI runs from paths containing spaces and hash characters", async () => {
|
|
243
|
-
const root = await mkdtemp(join(tmpdir(), "socketduct gateway #"))
|
|
244
|
-
const escapedScriptPath = join(root, "deploy gateway #.mjs")
|
|
245
|
-
try {
|
|
246
|
-
await copyFile(scriptPath, escapedScriptPath)
|
|
247
|
-
const result = spawnSync(process.execPath, [escapedScriptPath], {
|
|
248
|
-
encoding: "utf8",
|
|
249
|
-
env: {...process.env, SOCKETDUCT_IMAGE: "ghcr.io/kaspernj/socketduct:latest"}
|
|
250
207
|
})
|
|
251
208
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
209
|
+
expect(calls.length).toBe(3)
|
|
210
|
+
for (const {args, environment: childEnvironment} of calls) {
|
|
211
|
+
expect(args.filter((argument) => argument === "--profile").length).toBe(1)
|
|
212
|
+
expect(args[args.indexOf("--profile") + 1]).toBe("docker-host")
|
|
213
|
+
expect(childEnvironment.COMPOSE_PROFILES).toBe("")
|
|
214
|
+
}
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it("gateway deployment fails closed on a rendered image mismatch or build source", async () => {
|
|
218
|
+
const {deployGateway} = await import("../../../scripts/deploy-gateway.js")
|
|
219
|
+
const image = `ghcr.io/kaspernj/socketduct@sha256:${"c".repeat(64)}`
|
|
220
|
+
const renderedModels = [
|
|
221
|
+
{services: {"socketduct-relay": {image: "ghcr.io/kaspernj/socketduct:latest"}}},
|
|
222
|
+
{services: {"socketduct-relay": {build: {context: "."}, image}}},
|
|
223
|
+
{services: {}}
|
|
224
|
+
]
|
|
225
|
+
|
|
226
|
+
for (const rendered of renderedModels) {
|
|
227
|
+
let calls = 0
|
|
228
|
+
await expect(deployGateway({
|
|
229
|
+
environment: {},
|
|
230
|
+
image,
|
|
231
|
+
projectDirectory: "/srv/socketduct",
|
|
232
|
+
run: () => {
|
|
233
|
+
calls += 1
|
|
234
|
+
return JSON.stringify(rendered)
|
|
235
|
+
}
|
|
236
|
+
})).rejects.toThrow(/rendered.*socketduct-relay|build source/i)
|
|
237
|
+
expect(calls).toBe(1)
|
|
238
|
+
}
|
|
239
|
+
})
|
|
258
240
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
241
|
+
it("gateway deployment CLI runs from paths containing spaces and hash characters", async () => {
|
|
242
|
+
const root = await mkdtemp(join(tmpdir(), "socketduct gateway #"))
|
|
243
|
+
const escapedScriptPath = join(root, "deploy gateway #.mjs")
|
|
244
|
+
try {
|
|
245
|
+
await copyFile(scriptPath, escapedScriptPath)
|
|
246
|
+
const result = spawnSync(process.execPath, [escapedScriptPath], {
|
|
247
|
+
encoding: "utf8",
|
|
248
|
+
env: {...process.env, SOCKETDUCT_IMAGE: "ghcr.io/kaspernj/socketduct:latest"}
|
|
249
|
+
})
|
|
267
250
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
251
|
+
expect(result.status).toBe(1)
|
|
252
|
+
expect(result.stderr).toMatch(/immutable GHCR image/)
|
|
253
|
+
} finally {
|
|
254
|
+
await rm(root, {recursive: true, force: true})
|
|
255
|
+
}
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it("gateway documentation describes pinning, private GHCR login, and digest rollback", async () => {
|
|
259
|
+
const [dockerDocs, packageJson, runningDocs, securityDocs] = await Promise.all([
|
|
260
|
+
readFile(docsPath, "utf8"),
|
|
261
|
+
readFile(packagePath, "utf8"),
|
|
262
|
+
readFile(runningDocsPath, "utf8"),
|
|
263
|
+
readFile(securityDocsPath, "utf8")
|
|
264
|
+
])
|
|
265
|
+
const publicDocs = `${dockerDocs}\n${runningDocs}\n${securityDocs}`
|
|
266
|
+
|
|
267
|
+
expect(JSON.parse(packageJson).scripts["gateway:deploy"]).toBe("node scripts/deploy-gateway.js")
|
|
268
|
+
expect(dockerDocs).toMatch(/Node\.js 24/i)
|
|
269
|
+
expect(dockerDocs).toMatch(/node --version/)
|
|
270
|
+
expect(dockerDocs).toMatch(/npm --version/)
|
|
271
|
+
expect(dockerDocs).toMatch(/npm run gateway:deploy/)
|
|
272
|
+
expect(dockerDocs).toMatch(/--project-name socketduct-gateway/)
|
|
273
|
+
expect(dockerDocs).toMatch(/docker login ghcr\.io/)
|
|
274
|
+
expect(dockerDocs).toMatch(/rollback/i)
|
|
275
|
+
expect(dockerDocs).toMatch(/change.*digest/i)
|
|
276
|
+
expect(publicDocs).not.toMatch(/docker compose[^\n]*up[^\n]*--build/)
|
|
277
|
+
})
|
|
278
278
|
})
|