Skip to content

Commit 48853a6

Browse files
validate container configuration in wrangler (#9774)
* CC-5513 Validate instance type in wrangler Validate instance type against account limits to give early feedback to the user. * validate instance type in containers/deploy.ts * clean up * Update .changeset/twenty-seas-cross.md Co-authored-by: emily-shen <[email protected]> * refactor how limits are checked * fix test * Update changeset * Update packages/wrangler/src/cloudchamber/limits.ts Co-authored-by: emily-shen <[email protected]> * Apply suggestions from code review Co-authored-by: emily-shen <[email protected]> * update comment * fix tests --------- Co-authored-by: emily-shen <[email protected]>
1 parent 5bd0a19 commit 48853a6

File tree

14 files changed

+633
-263
lines changed

14 files changed

+633
-263
lines changed

.changeset/twenty-seas-cross.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"wrangler": patch
3+
---
4+
5+
Validate container configuration against account limits in wrangler to give early feedback to the user

packages/wrangler/src/__tests__/cloudchamber/build.test.ts

Lines changed: 42 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,11 @@ import {
66
getCloudflareContainerRegistry,
77
runDockerCmd,
88
} from "@cloudflare/containers-shared";
9-
import { ensureDiskLimits } from "../../cloudchamber/build";
109
import { UserError } from "../../errors";
1110
import { mockAccountId, mockApiToken } from "../helpers/mock-account-id";
1211
import { runInTempDir } from "../helpers/run-in-tmp";
1312
import { runWrangler } from "../helpers/run-wrangler";
1413
import { mockAccountV4 as mockAccount } from "./utils";
15-
import type { CompleteAccountCustomer } from "@cloudflare/containers-shared";
16-
17-
const MiB = 1024 * 1024;
1814

1915
vi.mock("@cloudflare/containers-shared", async (importOriginal) => {
2016
const actual = await importOriginal();
@@ -36,7 +32,11 @@ describe("buildAndMaybePush", () => {
3632

3733
beforeEach(() => {
3834
vi.clearAllMocks();
39-
vi.mocked(dockerImageInspect).mockResolvedValue("53387881 2 []");
35+
vi.mocked(dockerImageInspect)
36+
// return empty array of repo digests (i.e. image does not exist remotely)
37+
.mockResolvedValueOnce("[]")
38+
// return image size and number of layers
39+
.mockResolvedValueOnce("53387881 2");
4040
mkdirSync("./container-context");
4141

4242
writeFileSync("./container-context/Dockerfile", dockerfile);
@@ -65,11 +65,23 @@ describe("buildAndMaybePush", () => {
6565
],
6666
dockerfile,
6767
});
68-
expect(dockerImageInspect).toHaveBeenCalledWith("/custom/docker/path", {
69-
imageTag: `${getCloudflareContainerRegistry()}/test-app:tag`,
70-
formatString:
71-
"{{ .Size }} {{ len .RootFS.Layers }} {{json .RepoDigests}}",
72-
});
68+
expect(dockerImageInspect).toHaveBeenCalledTimes(2);
69+
expect(dockerImageInspect).toHaveBeenNthCalledWith(
70+
1,
71+
"/custom/docker/path",
72+
{
73+
imageTag: `${getCloudflareContainerRegistry()}/test-app:tag`,
74+
formatString: "{{json .RepoDigests}}",
75+
}
76+
);
77+
expect(dockerImageInspect).toHaveBeenNthCalledWith(
78+
2,
79+
"/custom/docker/path",
80+
{
81+
imageTag: `${getCloudflareContainerRegistry()}/test-app:tag`,
82+
formatString: "{{ .Size }} {{ len .RootFS.Layers }}",
83+
}
84+
);
7385
expect(runDockerCmd).toHaveBeenCalledWith("/custom/docker/path", [
7486
"push",
7587
`${getCloudflareContainerRegistry()}/some-account-id/test-app:tag`,
@@ -115,11 +127,14 @@ describe("buildAndMaybePush", () => {
115127
"rm",
116128
`${getCloudflareContainerRegistry()}/some-account-id/test-app:tag`,
117129
]);
118-
expect(dockerImageInspect).toHaveBeenCalledOnce();
119-
expect(dockerImageInspect).toHaveBeenCalledWith("docker", {
130+
expect(dockerImageInspect).toHaveBeenCalledTimes(2);
131+
expect(dockerImageInspect).toHaveBeenNthCalledWith(1, "docker", {
132+
imageTag: `${getCloudflareContainerRegistry()}/test-app:tag`,
133+
formatString: "{{json .RepoDigests}}",
134+
});
135+
expect(dockerImageInspect).toHaveBeenNthCalledWith(2, "docker", {
120136
imageTag: `${getCloudflareContainerRegistry()}/test-app:tag`,
121-
formatString:
122-
"{{ .Size }} {{ len .RootFS.Layers }} {{json .RepoDigests}}",
137+
formatString: "{{ .Size }} {{ len .RootFS.Layers }}",
123138
});
124139
expect(dockerLoginManagedRegistry).toHaveBeenCalledOnce();
125140
});
@@ -129,9 +144,12 @@ describe("buildAndMaybePush", () => {
129144
abort: () => {},
130145
ready: Promise.resolve({ aborted: false }),
131146
});
132-
vi.mocked(dockerImageInspect).mockResolvedValue(
133-
'53387881 2 ["registry.cloudflare.com/test-app@sha256:three"]'
134-
);
147+
vi.mocked(dockerImageInspect).mockReset();
148+
vi.mocked(dockerImageInspect)
149+
.mockResolvedValueOnce(
150+
'["registry.cloudflare.com/test-app@sha256:three"]'
151+
)
152+
.mockResolvedValueOnce("53387881 2");
135153
await runWrangler(
136154
"containers build ./container-context -t test-app:tag -p"
137155
);
@@ -165,11 +183,14 @@ describe("buildAndMaybePush", () => {
165183
"rm",
166184
`${getCloudflareContainerRegistry()}/test-app:tag`,
167185
]);
168-
expect(dockerImageInspect).toHaveBeenCalledOnce();
169-
expect(dockerImageInspect).toHaveBeenCalledWith("docker", {
186+
expect(dockerImageInspect).toHaveBeenCalledTimes(2);
187+
expect(dockerImageInspect).toHaveBeenNthCalledWith(1, "docker", {
170188
imageTag: `${getCloudflareContainerRegistry()}/test-app:tag`,
171-
formatString:
172-
"{{ .Size }} {{ len .RootFS.Layers }} {{json .RepoDigests}}",
189+
formatString: "{{json .RepoDigests}}",
190+
});
191+
expect(dockerImageInspect).toHaveBeenNthCalledWith(2, "docker", {
192+
imageTag: `${getCloudflareContainerRegistry()}/test-app:tag`,
193+
formatString: "{{ .Size }} {{ len .RootFS.Layers }}",
173194
});
174195
expect(dockerLoginManagedRegistry).toHaveBeenCalledOnce();
175196
});
@@ -266,44 +287,4 @@ describe("buildAndMaybePush", () => {
266287
runWrangler("containers build ./container-context -t test-app:tag -p")
267288
).rejects.toThrow(new UserError(errorMessage));
268289
});
269-
270-
describe("ensureDiskLimits", () => {
271-
const accountBase = {
272-
limits: { disk_mb_per_deployment: 2000 },
273-
} as CompleteAccountCustomer;
274-
275-
it("should throw error if app configured disk exceeds account limit", async () => {
276-
await expect(() =>
277-
ensureDiskLimits({
278-
requiredSizeInBytes: 333 * MiB, // 333MiB
279-
account: accountBase,
280-
configDiskInBytes: 3000 * MiB, // ie 3GB - this exceeds the account limit of 2GB
281-
})
282-
).rejects.toThrowErrorMatchingInlineSnapshot(
283-
`[Error: Exceeded account limits: Your container is configured to use a disk size of 3146MB. However, that exceeds the account limit of 2000MB]`
284-
);
285-
});
286-
287-
it("should throw error if image size exceeds allowed size", async () => {
288-
await expect(() =>
289-
ensureDiskLimits({
290-
requiredSizeInBytes: 3000 * MiB, // 3GiB
291-
account: accountBase,
292-
configDiskInBytes: undefined,
293-
})
294-
).rejects.toThrowErrorMatchingInlineSnapshot(
295-
`[Error: Image too large: needs 3146MB, but your app is limited to images with size 2000MB. Your account needs more disk size per instance to run this container. The default disk size is 2GB.]`
296-
);
297-
});
298-
299-
it("should not throw when disk size is within limits", async () => {
300-
const result = await ensureDiskLimits({
301-
requiredSizeInBytes: 256 * MiB, // 256MiB
302-
account: accountBase,
303-
configDiskInBytes: undefined,
304-
});
305-
306-
expect(result).toEqual(undefined);
307-
});
308-
});
309290
});

0 commit comments

Comments
 (0)