Skip to content

Commit d8c895a

Browse files
authored
Rename "wrangler_ssh" to "ssh" in Containers configuration (#13567)
"wrangler_ssh" is redundant since this option is set in the Wrangler config file. Rename it to just "ssh", while still supporting "wrangler_ssh" for backward compatibility.
1 parent 1aee990 commit d8c895a

6 files changed

Lines changed: 212 additions & 30 deletions

File tree

.changeset/calm-bobcats-chew.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"wrangler": minor
3+
---
4+
5+
Rename the documented containers SSH config option to `ssh`
6+
7+
Wrangler now accepts and documents `containers.ssh` in config files while continuing to accept `containers.wrangler_ssh` as an undocumented backwards-compatible alias. Wrangler still sends and reads `wrangler_ssh` when talking to the containers API.

packages/workers-utils/src/config/environment.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ export type ContainerApp = {
171171
disk_mb?: number;
172172
};
173173

174-
wrangler_ssh?: {
174+
ssh?: {
175175
/**
176176
* If enabled, those with write access to a container will be able to SSH into it through Wrangler.
177177
* @default false
@@ -184,6 +184,15 @@ export type ContainerApp = {
184184
port?: number;
185185
};
186186

187+
/**
188+
* @deprecated Use `ssh` instead.
189+
* @hidden
190+
*/
191+
wrangler_ssh?: {
192+
enabled: boolean;
193+
port?: number;
194+
};
195+
187196
/**
188197
* SSH public keys to put in the container's authorized_keys file.
189198
*/

packages/workers-utils/src/config/validation.ts

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import type { Config, DevConfig, RawConfig, RawDevConfig } from "./config";
4141
import type {
4242
Assets,
4343
CacheOptions,
44+
ContainerApp,
4445
DispatchNamespaceOutbound,
4546
Environment,
4647
Observability,
@@ -3335,6 +3336,11 @@ function validateContainerApp(
33353336
`"containers.durable_objects" is deprecated. Use the "class_name" field instead.`
33363337
);
33373338
}
3339+
if ("wrangler_ssh" in containerAppOptional) {
3340+
diagnostics.warnings.push(
3341+
`"containers.wrangler_ssh" is deprecated. Use "containers.ssh" instead.`
3342+
);
3343+
}
33383344

33393345
// unsafe.containers
33403346
if ("unsafe" in containerAppOptional) {
@@ -3365,6 +3371,7 @@ function validateContainerApp(
33653371
"class_name",
33663372
"scheduling_policy",
33673373
"instance_type",
3374+
"ssh",
33683375
"wrangler_ssh",
33693376
"authorized_keys",
33703377
"trusted_user_ca_keys",
@@ -3387,30 +3394,40 @@ function validateContainerApp(
33873394
);
33883395
}
33893396

3390-
if ("wrangler_ssh" in containerAppOptional) {
3391-
if (
3392-
!isRequiredProperty(
3393-
containerAppOptional.wrangler_ssh,
3394-
"enabled",
3395-
"boolean"
3396-
)
3397-
) {
3397+
let sshField: "ssh" | "wrangler_ssh" | undefined;
3398+
let sshConfig:
3399+
| ContainerApp["ssh"]
3400+
| ContainerApp["wrangler_ssh"]
3401+
| undefined;
3402+
3403+
if ("ssh" in containerAppOptional) {
3404+
sshField = "ssh";
3405+
sshConfig = containerAppOptional.ssh;
3406+
containerAppOptional.wrangler_ssh = containerAppOptional.ssh;
3407+
delete containerAppOptional.ssh;
3408+
} else if ("wrangler_ssh" in containerAppOptional) {
3409+
sshField = "wrangler_ssh";
3410+
sshConfig = containerAppOptional.wrangler_ssh;
3411+
}
3412+
3413+
if (sshField !== undefined) {
3414+
const sshConfigObject =
3415+
typeof sshConfig === "object" && sshConfig !== null ? sshConfig : {};
3416+
3417+
if (!isRequiredProperty(sshConfigObject, "enabled", "boolean")) {
33983418
diagnostics.errors.push(
3399-
`${field}.wrangler_ssh.enabled must be a boolean`
3419+
`${field}.${sshField}.enabled must be a boolean`
34003420
);
34013421
}
34023422

3423+
const sshPort =
3424+
"port" in sshConfigObject ? sshConfigObject.port : undefined;
34033425
if (
3404-
!isOptionalProperty(
3405-
containerAppOptional.wrangler_ssh,
3406-
"port",
3407-
"number"
3408-
) ||
3409-
containerAppOptional.wrangler_ssh.port < 1 ||
3410-
containerAppOptional.wrangler_ssh.port > 65535
3426+
!isOptionalProperty(sshConfigObject, "port", "number") ||
3427+
(typeof sshPort === "number" && (sshPort < 1 || sshPort > 65535))
34113428
) {
34123429
diagnostics.errors.push(
3413-
`${field}.wrangler_ssh.port must be a number between 1 and 65535 inclusive`
3430+
`${field}.${sshField}.port must be a number between 1 and 65535 inclusive`
34143431
);
34153432
}
34163433
}

packages/wrangler/src/__tests__/containers/deploy.test.ts

Lines changed: 104 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1824,7 +1824,7 @@ describe("wrangler deploy with containers", () => {
18241824
containers: [
18251825
{
18261826
...DEFAULT_CONTAINER_FROM_REGISTRY,
1827-
wrangler_ssh: {
1827+
ssh: {
18281828
enabled: true,
18291829
port: 1010,
18301830
},
@@ -1884,7 +1884,7 @@ describe("wrangler deploy with containers", () => {
18841884
│ image = "registry.cloudflare.com/some-account-id/hello:world"
18851885
│ instance_type = "lite"
18861886
1887-
│ [containers.configuration.wrangler_ssh]
1887+
│ [containers.configuration.ssh]
18881888
│ enabled = true
18891889
│ port = 1010
18901890
@@ -1916,7 +1916,7 @@ describe("wrangler deploy with containers", () => {
19161916
containers: [
19171917
{
19181918
...DEFAULT_CONTAINER_FROM_REGISTRY,
1919-
wrangler_ssh: {
1919+
ssh: {
19201920
enabled: true,
19211921
},
19221922
authorized_keys: [
@@ -1994,7 +1994,7 @@ describe("wrangler deploy with containers", () => {
19941994
│ + [[containers.configuration.authorized_keys]]
19951995
│ + name = "jeff"
19961996
│ + public_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC0chNcjRotdsxXTwPPNoqVCGn4EcEWdUkkBPNm/v4gm"
1997-
│ + [containers.configuration.wrangler_ssh]
1997+
│ + [containers.configuration.ssh]
19981998
│ + enabled = true
19991999
│ [containers.durable_objects]
20002000
│ namespace_id = "1"
@@ -2010,6 +2010,106 @@ describe("wrangler deploy with containers", () => {
20102010
`);
20112011
});
20122012

2013+
it("enables ssh when provided in wrangler.jsonc", async ({ expect }) => {
2014+
mockGetVersion("Galaxy-Class");
2015+
writeWranglerConfig(
2016+
{
2017+
...DEFAULT_DURABLE_OBJECTS,
2018+
containers: [
2019+
{
2020+
...DEFAULT_CONTAINER_FROM_REGISTRY,
2021+
ssh: {
2022+
enabled: true,
2023+
port: 2022,
2024+
},
2025+
},
2026+
],
2027+
},
2028+
"./wrangler.jsonc"
2029+
);
2030+
2031+
mockGetApplications([]);
2032+
2033+
mockCreateApplication(expect, {
2034+
name: "my-container",
2035+
max_instances: 10,
2036+
scheduling_policy: SchedulingPolicy.DEFAULT,
2037+
configuration: {
2038+
image: "registry.cloudflare.com/some-account-id/hello:world",
2039+
wrangler_ssh: {
2040+
enabled: true,
2041+
port: 2022,
2042+
},
2043+
},
2044+
});
2045+
2046+
await runWrangler("deploy index.js --config ./wrangler.jsonc");
2047+
2048+
expect(std.warn).toBe("");
2049+
expect(std.err).toBe("");
2050+
});
2051+
2052+
it("accepts wrangler_ssh as a backward-compatible alias", async ({
2053+
expect,
2054+
}) => {
2055+
mockGetVersion("Galaxy-Class");
2056+
writeWranglerConfig({
2057+
...DEFAULT_DURABLE_OBJECTS,
2058+
containers: [
2059+
{
2060+
...DEFAULT_CONTAINER_FROM_REGISTRY,
2061+
wrangler_ssh: {
2062+
enabled: true,
2063+
port: 2222,
2064+
},
2065+
},
2066+
],
2067+
});
2068+
2069+
mockGetApplications([]);
2070+
2071+
mockCreateApplication(expect, {
2072+
name: "my-container",
2073+
max_instances: 10,
2074+
scheduling_policy: SchedulingPolicy.DEFAULT,
2075+
configuration: {
2076+
image: "registry.cloudflare.com/some-account-id/hello:world",
2077+
wrangler_ssh: {
2078+
enabled: true,
2079+
port: 2222,
2080+
},
2081+
},
2082+
});
2083+
2084+
await runWrangler("deploy index.js");
2085+
2086+
expect(std.warn).toContain("Processing wrangler.toml configuration:");
2087+
expect(std.warn).toContain(
2088+
'"containers.wrangler_ssh" is deprecated. Use "containers.ssh" instead.'
2089+
);
2090+
expect(std.err).toBe("");
2091+
});
2092+
2093+
it("should validate containers.ssh fields", async ({ expect }) => {
2094+
writeWranglerConfig({
2095+
...DEFAULT_DURABLE_OBJECTS,
2096+
containers: [
2097+
{
2098+
...DEFAULT_CONTAINER_FROM_REGISTRY,
2099+
ssh: {
2100+
// @ts-expect-error - intentionally invalid to test config validation
2101+
enabled: "true",
2102+
port: 70000,
2103+
},
2104+
},
2105+
],
2106+
});
2107+
2108+
await expect(runWrangler("deploy index.js")).rejects.toThrow(
2109+
/containers\.ssh\.enabled must be a boolean[\s\S]*containers\.ssh\.port must be a number between 1 and 65535 inclusive/
2110+
);
2111+
});
2112+
20132113
describe("ctx.exports", async () => {
20142114
// note how mockGetVersion is NOT mocked in any of these, unlike the other tests.
20152115
// instead we mock the list durable objects endpoint, which the ctx.exports path uses instead
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import fs from "node:fs";
2+
import path from "node:path";
3+
import { describe, it } from "vitest";
4+
5+
describe("containers config schema", () => {
6+
it("documents ssh without exposing wrangler_ssh", ({ expect }) => {
7+
const schemaFile = path.join(__dirname, "../../../config-schema.json");
8+
const schema = JSON.parse(fs.readFileSync(schemaFile, "utf-8")) as {
9+
definitions: {
10+
ContainerApp: {
11+
properties: Record<string, unknown>;
12+
};
13+
};
14+
};
15+
16+
expect(schema.definitions.ContainerApp.properties).toHaveProperty("ssh");
17+
expect(schema.definitions.ContainerApp.properties).not.toHaveProperty(
18+
"wrangler_ssh"
19+
);
20+
});
21+
});

packages/wrangler/src/containers/deploy.ts

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,36 @@ function containerConfigToCreateRequest(
315315
};
316316
}
317317

318+
function formatContainerSnippetForDisplay<
319+
T extends {
320+
configuration?: ModifyApplicationRequestBody["configuration"];
321+
},
322+
>(container: T, configPath: Config["configPath"]) {
323+
// Normalize field names from the API into the Wrangler specific format
324+
// Example: `container.configuration.wrangler_ssh` (API) => `container.configuration.ssh` (Wrangler)
325+
const configurationForDisplay =
326+
container.configuration === undefined
327+
? undefined
328+
: Object.fromEntries(
329+
Object.entries(container.configuration).map(([key, value]) => [
330+
key === "wrangler_ssh" ? "ssh" : key,
331+
value,
332+
])
333+
);
334+
335+
return formatConfigSnippet(
336+
{
337+
containers: [
338+
{
339+
...container,
340+
configuration: configurationForDisplay,
341+
} as unknown as ContainerApp,
342+
],
343+
},
344+
configPath
345+
);
346+
}
347+
318348
export async function apply(
319349
args: {
320350
imageRef: ImageRef;
@@ -404,15 +434,13 @@ export async function apply(
404434
sortObjectRecursive<ModifyApplicationRequestBody>(modifyReq)
405435
);
406436

407-
const prev = formatConfigSnippet(
408-
// note this really is a CreateApplicationRequest, not a ContainerApp
409-
// but this function doesn't actually care about the type
410-
{ containers: [normalisedPrevApp as ContainerApp] },
437+
const prev = formatContainerSnippetForDisplay(
438+
normalisedPrevApp,
411439
config.configPath
412440
);
413441

414-
const now = formatConfigSnippet(
415-
{ containers: [nowContainer as ContainerApp] },
442+
const now = formatContainerSnippetForDisplay(
443+
nowContainer,
416444
config.configPath
417445
);
418446
const diff = new Diff(prev, now);
@@ -453,8 +481,8 @@ export async function apply(
453481
// print the header of the app
454482
updateStatus(bold.underline(green.underline("NEW")) + ` ${appConfig.name}`);
455483

456-
const configStr = formatConfigSnippet(
457-
{ containers: [appConfig as ContainerApp] },
484+
const configStr = formatContainerSnippetForDisplay(
485+
appConfig,
458486
config.configPath
459487
);
460488

0 commit comments

Comments
 (0)