Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .changeset/five-drinks-stick.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,3 @@
stable `ratelimit` binding

[Rate Limiting in Workers ](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/) is now generally available, `ratelimit` can be removed from unsafe bindings.

6 changes: 6 additions & 0 deletions .changeset/spotty-plums-speak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@cloudflare/containers-shared": minor
"wrangler": patch
---

Add `containers ssh` command
1 change: 1 addition & 0 deletions packages/containers-shared/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from "./src/types";
export * from "./src/inspect";
export * from "./src/registry";
export * from "./src/images";
export * from "./src/ssh";
3 changes: 3 additions & 0 deletions packages/containers-shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
"test:watch": "pnpm run test --testTimeout=50000 --watch",
"type:tests": "tsc -p ./tests/tsconfig.json"
},
"dependencies": {
"undici": "catalog:default"
},
"devDependencies": {
"@cloudflare/eslint-config-shared": "workspace:*",
"@cloudflare/workers-tsconfig": "workspace:*",
Expand Down
4 changes: 4 additions & 0 deletions packages/containers-shared/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type { AddressAssignment } from "./models/AddressAssignment";
export type { Application } from "./models/Application";
export type { ApplicationAffinities } from "./models/ApplicationAffinities";
export { ApplicationAffinityColocation } from "./models/ApplicationAffinityColocation";
export { ApplicationAffinityHardwareGeneration } from "./models/ApplicationAffinityHardwareGeneration";
export type { ApplicationConstraintPop } from "./models/ApplicationConstraintPop";
export type { ApplicationConstraints } from "./models/ApplicationConstraints";
export type { ApplicationHealth } from "./models/ApplicationHealth";
Expand Down Expand Up @@ -192,13 +193,16 @@ export { SecretNotFound } from "./models/SecretNotFound";
export type { SSHPublicKey } from "./models/SSHPublicKey";
export type { SSHPublicKeyID } from "./models/SSHPublicKeyID";
export type { SSHPublicKeyItem } from "./models/SSHPublicKeyItem";
export type { SSHPublicKeyItemV3 } from "./models/SSHPublicKeyItemV3";
export { SSHPublicKeyNotFoundError } from "./models/SSHPublicKeyNotFoundError";
export type { UnAuthorizedError } from "./models/UnAuthorizedError";
export type { UnixTimestamp } from "./models/UnixTimestamp";
export type { UnknownAccount } from "./models/UnknownAccount";
export { UpdateApplicationRolloutRequest } from "./models/UpdateApplicationRolloutRequest";
export type { UpdateRolloutResponse } from "./models/UpdateRolloutResponse";
export type { UserDeploymentConfiguration } from "./models/UserDeploymentConfiguration";
export type { WranglerSSHConfig } from "./models/WranglerSSHConfig";
export type { WranglerSSHResponse } from "./models/WranglerSSHResponse";

export { AccountService } from "./services/AccountService";
export { ApplicationsService } from "./services/ApplicationsService";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import type { Observability } from "./Observability";
import type { Placement } from "./Placement";
import type { Ref } from "./Ref";
import type { SSHPublicKeyID } from "./SSHPublicKeyID";
import type { SSHPublicKeyItemV3 } from "./SSHPublicKeyItemV3";
import type { WranglerSSHConfig } from "./WranglerSSHConfig";

/**
* A Deployment represents an intent to run one or many containers, with the same image, in a particular location or region.
Expand All @@ -41,6 +43,8 @@ export type DeploymentV2 = {
type: DeploymentType;
image: Image;
location: DeploymentLocation;
wrangler_ssh?: WranglerSSHConfig;
authorized_keys?: Array<SSHPublicKeyItemV3>;
/**
* A list of SSH public key IDs from the account
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@ import type { Observability } from "./Observability";
import type { Port } from "./Port";
import type { ProvisionerConfiguration } from "./ProvisionerConfiguration";
import type { SSHPublicKeyID } from "./SSHPublicKeyID";
import type { SSHPublicKeyItemV3 } from "./SSHPublicKeyItemV3";
import type { WranglerSSHConfig } from "./WranglerSSHConfig";

/**
* Properties required to modify a cloudchamber deployment specified by the user.
*/
export type ModifyUserDeploymentConfiguration = {
image?: Image;
wrangler_ssh?: WranglerSSHConfig;
authorized_keys?: Array<SSHPublicKeyItemV3>;
/**
* A list of SSH public key IDs from the account
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */

Comment on lines +1 to +4
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really needed?
If not, can this be removed from all the added files?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is all auto-generated code that's being copy-pasted over, we should remove these ignores but i can do that in a separate PR - its not really on @flakey5 :')

import type { SSHPublicKey } from "./SSHPublicKey";

/**
* An SSH public key attached to a specific application or account.
*/
export type SSHPublicKeyItemV3 = {
name: string;
public_key: SSHPublicKey;
};
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@ import type { Observability } from "./Observability";
import type { Port } from "./Port";
import type { ProvisionerConfiguration } from "./ProvisionerConfiguration";
import type { SSHPublicKeyID } from "./SSHPublicKeyID";
import type { SSHPublicKeyItemV3 } from "./SSHPublicKeyItemV3";
import type { WranglerSSHConfig } from "./WranglerSSHConfig";

/**
* Properties required to create a cloudchamber deployment specified by the user
*/
export type UserDeploymentConfiguration = {
image: Image;
wrangler_ssh?: WranglerSSHConfig;
authorized_keys?: Array<SSHPublicKeyItemV3>;
/**
* A list of SSH public key IDs from the account
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */

/**
* Configuration properties for SSH'ing into a container with Wrangler
*/
export type WranglerSSHConfig = {
enabled: boolean;
port?: number;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */

export type WranglerSSHResponse = {
url: string;
token: string;
};
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type { ModifyDeploymentV2RequestBody } from "../models/ModifyDeploymentV2
import type { ModifyUserDeploymentConfiguration } from "../models/ModifyUserDeploymentConfiguration";
import type { PlacementID } from "../models/PlacementID";
import type { ReplaceDeploymentRequestBody } from "../models/ReplaceDeploymentRequestBody";
import type { WranglerSSHResponse } from "../models/WranglerSSHResponse";

export class DeploymentsService {
/**
Expand Down Expand Up @@ -81,6 +82,31 @@ export class DeploymentsService {
});
}

/**
* Get credentials to SSH into a Container
* Get a JWT to hit the SSH port on a given container.
* @param instanceId
* @returns WranglerSSHResponse Credentials to SSH into a Container
* @throws ApiError
*/
public static containerWranglerSsh(
instanceId: DeploymentID
): CancelablePromise<WranglerSSHResponse> {
return __request(OpenAPI, {
method: "GET",
url: "/instances/{instance_id}/ssh",
path: {
instance_id: instanceId,
},
errors: {
400: `Unknown account`,
401: `Unauthorized`,
404: `Deployment not found`,
500: `There has been an internal error`,
},
});
}

/**
* Create a new deployment
* Creates a new deployment. A Deployment represents an intent to run one container, with image, in a particular location
Expand Down
84 changes: 84 additions & 0 deletions packages/containers-shared/src/ssh.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { spawn } from "node:child_process";
import { createServer } from "node:net";
import { WebSocket } from "undici";
import type { WranglerSSHResponse } from "./client";
import type { Server } from "node:net";

export function verifySshInstalled(sshPath: string): Promise<undefined> {
return new Promise<undefined>((resolve, reject) => {
const child = spawn(sshPath, ["-V"], {
detached: true,
});

let errorHandled = false;
child.on("close", (code) => {
if (code === 0) {
resolve(undefined);
} else if (!errorHandled) {
errorHandled = true;
reject(new Error(`ssh exited with status code: ${code}`));
}
});

child.on("error", (err) => {
if (!errorHandled) {
errorHandled = true;
reject(new Error(`verifying ssh installation failed: ${err.message}`));
}
});
});
}

/**
* Creates a local TCP proxy that wraps data sent in a websocket binary
* websocket message
*/
export function createSshTcpProxy(sshResponse: WranglerSSHResponse): Server {
let hasConnection = false;
const proxy = createServer((inbound) => {
if (hasConnection) {
inbound.end();
return;
}

hasConnection = true;

const ws = new WebSocket(sshResponse.url, {
headers: {
authorization: `Bearer ${sshResponse.token}`,
"user-agent": "wrangler",
},
});

ws.addEventListener("error", (err) => {
console.error("Web socket error:", err.error);
inbound.end();
proxy.close();
});

ws.addEventListener("open", () => {
inbound.on("data", (data) => {
ws.send(data);
});

inbound.on("close", () => {
ws.close();
proxy.close();
});

ws.addEventListener("message", async ({ data }) => {
const arrayBuffer = await data.arrayBuffer();
const arr = new Uint8Array(arrayBuffer);

inbound.write(arr);
});

ws.addEventListener("close", () => {
inbound.end();
proxy.close();
});
});
});

return proxy;
}
4 changes: 4 additions & 0 deletions packages/containers-shared/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import type {
ApplicationAffinityColocation,
InstanceType,
SchedulingPolicy,
SSHPublicKeyItemV3,
WranglerSSHConfig,
} from "./client";
import type { ApplicationAffinityHardwareGeneration } from "./client/models/ApplicationAffinityHardwareGeneration";

Expand Down Expand Up @@ -71,6 +73,8 @@ export type SharedContainerConfig = {
/** if undefined in config, defaults to "full_auto" */
rollout_kind: "full_auto" | "full_manual" | "none";
rollout_active_grace_period: number;
wrangler_ssh?: WranglerSSHConfig;
authorized_keys?: Array<SSHPublicKeyItemV3>;
constraints: {
regions?: string[];
cities?: string[];
Expand Down
50 changes: 50 additions & 0 deletions packages/wrangler/src/__tests__/containers/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -650,4 +650,54 @@ describe("getNormalizedContainerOptions", () => {
expect(result).toHaveLength(1);
expect(result[0].rollout_step_percentage).toBe(100);
});

it("should handle valid ssh and authorized_keys config", async () => {
const config: Config = {
name: "test-worker",
configPath: "/test/wrangler.toml",
topLevelName: "test-worker",
containers: [
{
class_name: "TestContainer",
image: `${getCloudflareContainerRegistry()}/test:latest`,
name: "test-container",
max_instances: 3,
wrangler_ssh: {
enabled: true,
port: 22,
},
authorized_keys: [
{
name: "blahaj",
public_key:
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC0chNcjRotdsxXTwPPNoqVCGn4EcEWdUkkBPNm/v4gm",
},
],
},
],
durable_objects: {
bindings: [
{
name: "TEST_DO",
class_name: "TestContainer",
},
],
},
} as Partial<Config> as Config;

const result = await getNormalizedContainerOptions(config, {});

expect(result).toHaveLength(1);
expect(result[0].wrangler_ssh).toMatchObject({
enabled: true,
port: 22,
});
expect(result[0].authorized_keys).toStrictEqual([
{
name: "blahaj",
public_key:
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC0chNcjRotdsxXTwPPNoqVCGn4EcEWdUkkBPNm/v4gm",
},
]);
});
});
Loading
Loading