Skip to content

Commit fdc3211

Browse files
committed
Add support for custom instance types
Custom instance types could previously only be set through setting vcpu/memory/disk through the `configuration` block. As we deprecate `configuration`, move setting vcpu, memory, and disk to the instance type field of wrangler. By nature, this enforces that custom instance types are mutually exclusive with the named instance types and will allow us to remove a lot of the logic that verifies this once `configuration` is removed.
1 parent 1e92dd8 commit fdc3211

File tree

5 files changed

+233
-19
lines changed

5 files changed

+233
-19
lines changed

.changeset/hungry-turtles-beam.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+
Add support for custom instance types

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

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1614,6 +1614,63 @@ describe("cloudchamber apply", () => {
16141614
expect(std.stderr).toMatchInlineSnapshot(`""`);
16151615
});
16161616

1617+
test("can apply a simple application (custom instance type)", async () => {
1618+
setIsTTY(false);
1619+
writeWranglerConfig({
1620+
name: "my-container",
1621+
containers: [
1622+
{
1623+
name: "my-container-app",
1624+
instances: 3,
1625+
class_name: "DurableObjectClass",
1626+
instance_type: {
1627+
vcpu: 1,
1628+
memory_mib: 1024,
1629+
disk_mb: 2000,
1630+
},
1631+
image: "docker.io/beep:boop",
1632+
constraints: {
1633+
tier: 2,
1634+
},
1635+
},
1636+
],
1637+
});
1638+
mockGetApplications([]);
1639+
mockCreateApplication({ id: "abc" });
1640+
await runWrangler("cloudchamber apply");
1641+
expect(std.stdout).toMatchInlineSnapshot(`
1642+
"╭ Deploy a container application deploy changes to your application
1643+
1644+
│ Container application changes
1645+
1646+
├ NEW my-container-app
1647+
1648+
│ [[containers]]
1649+
│ name = \\"my-container-app\\"
1650+
│ instances = 3
1651+
│ scheduling_policy = \\"default\\"
1652+
1653+
│ [containers.constraints]
1654+
│ tier = 2
1655+
1656+
│ [containers.configuration]
1657+
│ image = \\"docker.io/beep:boop\\"
1658+
│ vcpu = 1
1659+
│ memory_mib = 1_024
1660+
1661+
│ [containers.configuration.disk]
1662+
│ size_mb = 2_000
1663+
1664+
1665+
│ SUCCESS Created application my-container-app (Application ID: abc)
1666+
1667+
╰ Applied changes
1668+
1669+
"
1670+
`);
1671+
expect(std.stderr).toMatchInlineSnapshot(`""`);
1672+
});
1673+
16171674
test("can apply a simple existing application (instance type)", async () => {
16181675
setIsTTY(false);
16191676
writeWranglerConfig({
@@ -1691,6 +1748,96 @@ describe("cloudchamber apply", () => {
16911748
expect(app.configuration?.instance_type).toEqual("standard");
16921749
});
16931750

1751+
test("can apply a simple existing application (custom instance type)", async () => {
1752+
setIsTTY(false);
1753+
writeWranglerConfig({
1754+
name: "my-container",
1755+
containers: [
1756+
{
1757+
name: "my-container-app",
1758+
instances: 4,
1759+
class_name: "DurableObjectClass",
1760+
instance_type: {
1761+
vcpu: 1,
1762+
memory_mib: 1024,
1763+
disk_mb: 6000,
1764+
},
1765+
image: "docker.io/beep:boop",
1766+
constraints: {
1767+
tier: 2,
1768+
},
1769+
},
1770+
],
1771+
});
1772+
mockGetApplications([
1773+
{
1774+
id: "abc",
1775+
name: "my-container-app",
1776+
instances: 3,
1777+
created_at: new Date().toString(),
1778+
version: 1,
1779+
account_id: "1",
1780+
scheduling_policy: SchedulingPolicy.REGIONAL,
1781+
configuration: {
1782+
image: "docker.io/beep:boop",
1783+
disk: {
1784+
size: "2GB",
1785+
size_mb: 2000,
1786+
},
1787+
vcpu: 0.0625,
1788+
memory: "256MB",
1789+
memory_mib: 256,
1790+
},
1791+
constraints: {
1792+
tier: 3,
1793+
},
1794+
},
1795+
]);
1796+
const applicationReqBodyPromise = mockModifyApplication();
1797+
await runWrangler("cloudchamber apply");
1798+
expect(std.stdout).toMatchInlineSnapshot(`
1799+
"╭ Deploy a container application deploy changes to your application
1800+
1801+
│ Container application changes
1802+
1803+
├ EDIT my-container-app
1804+
1805+
│ [[containers]]
1806+
│ - instances = 3
1807+
│ + instances = 4
1808+
│ name = \\"my-container-app\\"
1809+
│ scheduling_policy = \\"regional\\"
1810+
1811+
│ [containers.configuration]
1812+
│ image = \\"docker.io/beep:boop\\"
1813+
│ memory = \\"256MB\\"
1814+
│ - memory_mib = 256
1815+
│ + memory_mib = 1_024
1816+
1817+
│ - vcpu = 0.0625
1818+
│ + vcpu = 1
1819+
1820+
│ [containers.configuration.disk]
1821+
│ size = \\"2GB\\"
1822+
│ - size_mb = 2_000
1823+
│ + size_mb = 6_000
1824+
1825+
│ [containers.constraints]
1826+
│ - tier = 3
1827+
│ + tier = 2
1828+
1829+
1830+
│ SUCCESS Modified application my-container-app
1831+
1832+
╰ Applied changes
1833+
1834+
"
1835+
`);
1836+
expect(std.stderr).toMatchInlineSnapshot(`""`);
1837+
const app = await applicationReqBodyPromise;
1838+
expect(app.configuration?.instance_type).toBeUndefined();
1839+
});
1840+
16941841
test("falls back on dev instance type when instance type is absent", async () => {
16951842
setIsTTY(false);
16961843
writeWranglerConfig({

packages/wrangler/src/cloudchamber/apply.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -188,22 +188,34 @@ function observabilityToConfiguration(
188188

189189
function containerAppToInstanceType(
190190
containerApp: ContainerApp
191-
): InstanceType | undefined {
191+
): Partial<UserDeploymentConfiguration> {
192+
let configuration = (containerApp.configuration ??
193+
{}) as Partial<UserDeploymentConfiguration>;
194+
192195
if (containerApp.instance_type !== undefined) {
193-
return containerApp.instance_type as InstanceType;
196+
if (typeof containerApp.instance_type === "string") {
197+
return { instance_type: containerApp.instance_type as InstanceType };
198+
}
199+
200+
configuration = {
201+
vcpu: containerApp.instance_type.vcpu,
202+
memory_mib: containerApp.instance_type.memory_mib,
203+
disk: {
204+
size_mb: containerApp.instance_type.disk_mb,
205+
},
206+
};
194207
}
195208

196209
// if no other configuration is set, we fall back to the default "dev" instance type
197-
const configuration =
198-
containerApp.configuration as UserDeploymentConfiguration;
199210
if (
200-
configuration.disk === undefined &&
211+
configuration.disk?.size_mb === undefined &&
201212
configuration.vcpu === undefined &&
202-
configuration.memory === undefined &&
203213
configuration.memory_mib === undefined
204214
) {
205-
return InstanceType.DEV;
215+
return { instance_type: InstanceType.DEV };
206216
}
217+
218+
return configuration;
207219
}
208220

209221
function containerAppToCreateApplication(
@@ -220,8 +232,8 @@ function containerAppToCreateApplication(
220232
const instanceType = containerAppToInstanceType(containerApp);
221233
const configuration: UserDeploymentConfiguration = {
222234
...(containerApp.configuration as UserDeploymentConfiguration),
235+
...instanceType,
223236
observability: observabilityConfiguration,
224-
instance_type: instanceType,
225237
};
226238

227239
// this should have been set to a default value of worker-name-class-name if unspecified by the user

packages/wrangler/src/config/environment.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,15 @@ export type ContainerApp = {
103103
* @optional
104104
* @default "dev"
105105
*/
106-
instance_type?: "dev" | "basic" | "standard";
106+
instance_type?:
107+
| "dev"
108+
| "basic"
109+
| "standard"
110+
| {
111+
vcpu?: number;
112+
memory_mib?: number;
113+
disk_mb?: number;
114+
};
107115

108116
/**
109117
* @deprecated Use top level `containers` fields instead.

packages/wrangler/src/config/validation.ts

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2428,13 +2428,13 @@ function validateContainerApp(
24282428
!containerAppOptional.image
24292429
) {
24302430
diagnostics.errors.push(
2431-
`"containers.image" field must be defined for each container app. This should be the path to your Dockerfile or a image URI pointing to the Cloudflare registry.`
2431+
`"containers.image" field must be defined for each container app. This should be the path to your Dockerfile or an image URI pointing to the Cloudflare registry.`
24322432
);
24332433
}
24342434

24352435
if ("configuration" in containerAppOptional) {
24362436
diagnostics.warnings.push(
2437-
`"containers.configuration" is deprecated. Use top level "containers" fields instead. "configuration.image" should be "image", "configuration.disk" should be set via "instance_type".`
2437+
`"containers.configuration" is deprecated. Use top level "containers" fields instead. "configuration.image" should be "image", limits should be set via "instance_type".`
24382438
);
24392439
if (
24402440
typeof containerAppOptional.configuration !== "object" ||
@@ -2509,14 +2509,6 @@ function validateContainerApp(
25092509
"string",
25102510
["full_auto", "full_manual", "none"]
25112511
);
2512-
validateOptionalProperty(
2513-
diagnostics,
2514-
field,
2515-
"instance_type",
2516-
containerAppOptional.instance_type,
2517-
"string",
2518-
["dev", "basic", "standard"]
2519-
);
25202512
validateOptionalProperty(
25212513
diagnostics,
25222514
field,
@@ -2589,6 +2581,56 @@ function validateContainerApp(
25892581
["image", "secrets", "labels", "disk", "vcpu", "memory_mib"]
25902582
);
25912583
}
2584+
2585+
// Instance Type validation: When present, the instance type should be either (1) a string
2586+
// representing a predefined instance type or (2) an object that optionally defines vcpu,
2587+
// memory, and disk.
2588+
//
2589+
// If an instance type is not set, a 'dev' instance type will be used. If a custom instance
2590+
// type doesn't set a value, that value will default to the corresponding value in a 'dev'
2591+
// instance type
2592+
if (typeof containerAppOptional.instance_type === "string") {
2593+
// validate named instance type
2594+
validateOptionalProperty(
2595+
diagnostics,
2596+
field,
2597+
"instance_type",
2598+
containerAppOptional.instance_type,
2599+
"string",
2600+
["dev", "basic", "standard"]
2601+
);
2602+
} else if (
2603+
validateOptionalProperty(
2604+
diagnostics,
2605+
field,
2606+
"instance_type",
2607+
containerAppOptional.instance_type,
2608+
"object"
2609+
) &&
2610+
containerAppOptional.instance_type
2611+
) {
2612+
// validate custom instance type
2613+
const instanceTypeProperties = ["vcpu", "memory_mib", "disk_mb"];
2614+
instanceTypeProperties.forEach((key) => {
2615+
if (
2616+
!isOptionalProperty(
2617+
containerAppOptional.instance_type,
2618+
key,
2619+
"number"
2620+
)
2621+
) {
2622+
diagnostics.errors.push(
2623+
`"containers.instance_type.${key}", when present, should be a number.`
2624+
);
2625+
}
2626+
});
2627+
validateAdditionalProperties(
2628+
diagnostics,
2629+
`${field}.instance_type`,
2630+
Object.keys(containerAppOptional.instance_type),
2631+
instanceTypeProperties
2632+
);
2633+
}
25922634
}
25932635

25942636
if (diagnostics.errors.length > 0) {

0 commit comments

Comments
 (0)