Skip to content

Commit

Permalink
Add default image for cloudchamber create
Browse files Browse the repository at this point in the history
Add a default image when using "wrangler cloudchamber create". This
image is docker.io/cloudflare/hello-world which is a simple HTTP server
that runs on Cloudflare's container platform.

This commit also augments validation to both the `cloudchamber create`
and `cloudchamber modify` commands to ensure the provided image contains
a tag and adheres to the format in the OCI specification.
  • Loading branch information
gpanders committed Nov 20, 2024
1 parent 563439b commit bf2e6b0
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 14 deletions.
5 changes: 5 additions & 0 deletions .changeset/six-apes-pull.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"wrangler": minor
---

Add a default image for cloudchamber create and modify commands
41 changes: 41 additions & 0 deletions packages/wrangler/src/__tests__/cloudchamber/common.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { parseImageName } from "../../cloudchamber/common";

describe("parseImageName", () => {
it("works", () => {
type TestCase = [input: string, name: string | undefined, tag: string | undefined, err: boolean];
const cases: TestCase[] = [
// Multiple domains
["docker.io/cloudflare/hello-world:1.0", "docker.io/cloudflare/hello-world", "1.0", false],

// Domain with port
["localhost:7777/web:local", "localhost:7777/web", "local", false],

// No domain
["hello-world:1.0", "hello-world", "1.0", false],

// Invalid name
["bad image name:1", undefined, undefined, true],

// Missing tag
["no-tag", undefined, undefined, true],
["no-tag:", undefined, undefined, true],

// Invalid tag
["no-tag::", undefined, undefined, true],

// latest tag
["name:latest", undefined, undefined, true],

// Too many colons
["registry.com:1234/foobar:4444/image:sometag", undefined, undefined, true],
];

for (const c of cases) {
let [input, name, tag, isErr] = c;
let result = parseImageName(input);
expect(result.name).toEqual(name);
expect(result.tag).toEqual(tag);
expect(result.err !== undefined).toEqual(isErr);
}
});
});
16 changes: 16 additions & 0 deletions packages/wrangler/src/__tests__/cloudchamber/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,22 @@ describe("cloudchamber create", () => {
);
});

it("should fail with a nice message when image is invalid", async () => {
setIsTTY(false);
setWranglerConfig({});
await expect(
runWrangler("cloudchamber create --image hello:latest --location sfo06")
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: "latest" tag is not allowed]`
);

await expect(
runWrangler("cloudchamber create --image hello --location sfo06")
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: Invalid image format: expected NAME:TAG]`
);
});

it("should fail with a nice message when parameters are mistyped", async () => {
setIsTTY(false);
fs.writeFileSync(
Expand Down
29 changes: 29 additions & 0 deletions packages/wrangler/src/cloudchamber/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,35 @@ import type { Arg } from "@cloudflare/cli/interactive";

export type CommonCloudchamberConfiguration = { json: boolean };

/**
* These patterns are defined by the OCI specification.
*
* See: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#pulling-manifests
*/
const alphaNumeric = "[a-z0-9]+";
const separator = "(?:\\.|_|__|-+)";
const port = ":[0-9]+";
const domain = `${alphaNumeric}(?:${separator}${alphaNumeric})*`;
const tag = "[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}";
const imageRe = new RegExp(`^((?:${domain}(?:${port})?/)?(?:${domain}/)*(?:${domain})):(${tag})$`);

/**
* Parse a container image name.
*/
export function parseImageName(value: string): {name?: string, tag?: string, err?: string} {
const matches = value.match(imageRe);
if (matches === null || matches.length != 3) {
return {err: "Invalid image format: expected NAME:TAG"};
}

const [_match, name, tag] = matches;
if (tag == "latest") {
return {err: '"latest" tag is not allowed'};
}

return {name, tag};
}

/**
* Wrapper that parses wrangler configuration and authentication.
* It also wraps exceptions and checks if they are from the RestAPI.
Expand Down
28 changes: 19 additions & 9 deletions packages/wrangler/src/cloudchamber/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
promptForLabels,
renderDeploymentConfiguration,
renderDeploymentMutationError,
parseImageName,
} from "./common";
import { wrap } from "./helpers/wrap";
import { loadAccount } from "./locations";
Expand All @@ -34,6 +35,8 @@ import type {
import type { EnvironmentVariable, Label, SSHPublicKeyID } from "./client";
import type { Arg } from "@cloudflare/cli/interactive";

const defaultContainerImage = "docker.io/cloudflare/hello-world:1.0";

export function createCommandOptionalYargs(yargs: CommonYargsArgvJSON) {
return yargs
.option("image", {
Expand Down Expand Up @@ -123,6 +126,12 @@ export async function createCommand(
}

const body = checkEverythingIsSet(args, ["image", "location"]);

const {err} = parseImageName(body.image);
if (err !== undefined) {
throw new Error(err);
}

const keysToAdd = args.allSshKeys
? (await pollSSHKeysUntilCondition(() => true)).map((key) => key.id)
: [];
Expand Down Expand Up @@ -239,18 +248,19 @@ async function handleCreateCommand(
label: "image",
validate: (value) => {
if (typeof value !== "string") {
return "unknown error";
return "Unknown error";
}
if (value.length === 0) {
return "you should fill this input";
}
if (value.endsWith(":latest")) {
return "we don't allow :latest tags";
// validate is called before defaultValue is
// applied, so we must set it ourselves
value = defaultContainerImage;
}

const {err} = parseImageName(value);
return err;
},
defaultValue: givenImage ?? "",
initialValue: givenImage ?? "",
helpText: 'i.e. "docker.io/org/app:1.2", :latest tags are not allowed!',
defaultValue: givenImage ?? defaultContainerImage,
helpText: 'NAME:TAG ("latest" tag is not allowed)',
type: "text",
});

Expand Down Expand Up @@ -323,4 +333,4 @@ async function handleCreateCommand(
await waitForPlacement(deployment);
}

const whichImageQuestion = "Which image url should we use for your container?";
const whichImageQuestion = "Which image should we use for your container?";
10 changes: 5 additions & 5 deletions packages/wrangler/src/cloudchamber/modify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
promptForLabels,
renderDeploymentConfiguration,
renderDeploymentMutationError,
parseImageName,
} from "./common";
import { wrap } from "./helpers/wrap";
import { loadAccount } from "./locations";
Expand Down Expand Up @@ -192,15 +193,14 @@ async function handleModifyCommand(
label: "",
validate: (value) => {
if (typeof value !== "string") {
return "unknown error";
}
if (value.endsWith(":latest")) {
return "we don't allow :latest tags";
return "Unknown error";
}
const {err} = parseImageName(value);
return err;
},
defaultValue: givenImage ?? deployment.image,
initialValue: givenImage ?? deployment.image,
helpText: "Press Return to leave unchanged",
helpText: "press Return to leave unchanged",
type: "text",
});

Expand Down

0 comments on commit bf2e6b0

Please sign in to comment.