Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add default image for cloudchamber create #7308

Merged
merged 1 commit into from
Nov 25, 2024
Merged
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
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
62 changes: 62 additions & 0 deletions packages/wrangler/src/__tests__/cloudchamber/common.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { parseImageName } from "../../cloudchamber/common";

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

// Domain with port
[
"localhost:7777/web:local",
{ name: "localhost:7777/web", tag: "local" },
],

// No domain
["hello-world:1.0", { name: "hello-world", tag: "1.0" }],

// With sha256 digest
[
"hello/world:1.0@sha256:abcdef0123456789",
{ name: "hello/world", tag: "1.0", digest: "abcdef0123456789" },
],

// sha256 digest but no tag
[
"hello/world@sha256:abcdef0123456789",
{ name: "hello/world", digest: "sha256:abcdef0123456789" },
],

// Invalid name
["bad image name:1", { err: true }],

// Missing tag
["no-tag", { err: true }],
["no-tag:", { err: true }],

// Invalid tag
["no-tag::", { err: true }],

// latest tag
["name:latest", { err: true }],

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

for (const c of cases) {
const [input, expected] = c;
const result = parseImageName(input);
expect(result.name).toEqual(expected.name);
expect(result.tag).toEqual(expected.tag);
expect(result.err !== undefined).toEqual(expected.err === true);
}
});
});
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[@DIGEST] or NAME@DIGEST]`
);
});

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

export type CommonCloudchamberConfiguration = { json: boolean };

/**
* Regular expression for matching an image name.
*
* See: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#pulling-manifests
*/
const imageRe = (() => {
const alphaNumeric = "[a-z0-9]+";
const separator = "(?:\\.|_|__|-+)";
const port = ":[0-9]+";
const domain = `${alphaNumeric}(?:${separator}${alphaNumeric})*`;
const name = `(?:${domain}(?:${port})?/)?(?:${domain}/)*(?:${domain})`;
const tag = ":([a-zA-Z0-9_][a-zA-Z0-9._-]{0,127})";
const digest = "@(sha256:[A-Fa-f0-9]+)";
const reference = `(?:${tag}(?:${digest})?|${digest})`;
return new RegExp(`^(${name})${reference}$`);
})();

/**
* Parse a container image name.
*/
export function parseImageName(value: string): {
name?: string;
tag?: string;
digest?: string;
err?: string;
} {
const matches = value.match(imageRe);
if (matches === null) {
return {
err: "Invalid image format: expected NAME:TAG[@DIGEST] or NAME@DIGEST",
};
}

const name = matches[1];
const tag = matches[2];
const digest = matches[3] ?? matches[4];

if (tag === "latest") {
return { err: '"latest" tag is not allowed' };
}

return { name, tag, digest };
}

/**
* 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 @@ -17,6 +17,7 @@ import {
collectLabels,
interactWithUser,
loadAccountSpinner,
parseImageName,
promptForEnvironmentVariables,
promptForLabels,
renderDeploymentConfiguration,
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 @@ -10,6 +10,7 @@ import {
collectLabels,
interactWithUser,
loadAccountSpinner,
parseImageName,
promptForEnvironmentVariables,
promptForLabels,
renderDeploymentConfiguration,
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
Loading