Skip to content

Commit c922a81

Browse files
authored
Add unsafe.containers (#11233)
* Add unsafe.containers * lint * Move unsafe field under main container config * fixups * fixup
1 parent a55c0e4 commit c922a81

File tree

6 files changed

+219
-8
lines changed

6 files changed

+219
-8
lines changed

.changeset/few-candies-melt.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@cloudflare/workers-utils": minor
3+
"wrangler": minor
4+
---
5+
6+
Add `containers.unsafe` to allow internal users to use additional container features

packages/workers-utils/src/config/environment.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,12 @@ export type ContainerApp = {
252252
* @default 0
253253
*/
254254
rollout_active_grace_period?: number;
255+
256+
/**
257+
* Directly passed to the API without wrangler-side validation or transformation.
258+
* @hidden
259+
*/
260+
unsafe?: Record<string, unknown>;
255261
};
256262

257263
/**
@@ -1078,7 +1084,6 @@ export interface EnvironmentNonInheritable {
10781084
metadata?: {
10791085
[key: string]: unknown;
10801086
};
1081-
10821087
/**
10831088
* Used for internal capnp uploads for the Workers runtime
10841089
*/

packages/workers-utils/src/config/validation.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2901,6 +2901,21 @@ function validateContainerApp(
29012901
);
29022902
}
29032903

2904+
// unsafe.containers
2905+
if ("unsafe" in containerAppOptional) {
2906+
if (
2907+
(containerAppOptional.unsafe &&
2908+
typeof containerAppOptional.unsafe !== "object") ||
2909+
Array.isArray(containerAppOptional.unsafe)
2910+
) {
2911+
diagnostics.errors.push(
2912+
`The field "containers.unsafe" should be an object but got ${JSON.stringify(
2913+
typeof containerAppOptional.unsafe
2914+
)}.`
2915+
);
2916+
}
2917+
}
2918+
29042919
validateAdditionalProperties(
29052920
diagnostics,
29062921
field,
@@ -2922,6 +2937,7 @@ function validateContainerApp(
29222937
"rollout_kind",
29232938
"durable_objects",
29242939
"rollout_active_grace_period",
2940+
"unsafe",
29252941
]
29262942
);
29272943
if ("configuration" in containerAppOptional) {

packages/wrangler/src/__tests__/config/configuration.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4796,6 +4796,35 @@ describe("normalizeAndValidateConfig()", () => {
47964796
});
47974797
});
47984798

4799+
it("should accept unsafe fields under containers", () => {
4800+
const { diagnostics } = normalizeAndValidateConfig(
4801+
{
4802+
containers: [
4803+
{
4804+
name: "test-container",
4805+
class_name: "TestContainer",
4806+
image: "registry.cloudflare.com/test:image",
4807+
unsafe: {
4808+
custom_field: "value",
4809+
},
4810+
},
4811+
],
4812+
} as unknown as RawConfig,
4813+
undefined,
4814+
undefined,
4815+
{ env: undefined }
4816+
);
4817+
4818+
expect(diagnostics.renderWarnings()).toMatchInlineSnapshot(`
4819+
"Processing wrangler configuration:
4820+
"
4821+
`);
4822+
expect(diagnostics.renderErrors()).toMatchInlineSnapshot(`
4823+
"Processing wrangler configuration:
4824+
"
4825+
`);
4826+
});
4827+
47994828
describe("[placement]", () => {
48004829
it(`should error if placement hint is set with placement mode "off"`, () => {
48014830
const { diagnostics } = normalizeAndValidateConfig(

packages/wrangler/src/__tests__/containers/deploy.test.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1858,6 +1858,127 @@ describe("wrangler deploy with containers dry run", () => {
18581858
});
18591859
});
18601860

1861+
describe("containers.unsafe configuration", () => {
1862+
runInTempDir();
1863+
const std = mockConsoleMethods();
1864+
mockAccountId();
1865+
mockApiToken();
1866+
1867+
beforeEach(() => {
1868+
setupCommonMocks();
1869+
fs.writeFileSync(
1870+
"index.js",
1871+
`export class ExampleDurableObject {}; export default{};`
1872+
);
1873+
});
1874+
1875+
it("should merge containers.unsafe config into create request", async () => {
1876+
mockGetVersion("Galaxy-Class");
1877+
writeWranglerConfig({
1878+
...DEFAULT_DURABLE_OBJECTS,
1879+
containers: [
1880+
{
1881+
...DEFAULT_CONTAINER_FROM_REGISTRY,
1882+
unsafe: {
1883+
custom_field: "custom_value",
1884+
nested: { field: "nested_value" },
1885+
configuration: { network: "nested_value" },
1886+
},
1887+
},
1888+
],
1889+
});
1890+
1891+
mockGetApplications([]);
1892+
1893+
mockCreateApplication({
1894+
name: "my-container",
1895+
max_instances: 10,
1896+
custom_field: "custom_value",
1897+
nested: { field: "nested_value" },
1898+
configuration: {
1899+
// @ts-expect-error - testing with custom unsafe fields
1900+
network: "nested_value",
1901+
image: "registry.cloudflare.com/some-account-id/hello:world",
1902+
},
1903+
});
1904+
1905+
await runWrangler("deploy index.js");
1906+
1907+
expect(std.err).toMatchInlineSnapshot(`""`);
1908+
});
1909+
1910+
it("should merge containers.unsafe config into modify request", async () => {
1911+
mockGetVersion("Galaxy-Class");
1912+
writeWranglerConfig({
1913+
...DEFAULT_DURABLE_OBJECTS,
1914+
containers: [
1915+
{
1916+
...DEFAULT_CONTAINER_FROM_REGISTRY,
1917+
max_instances: 20,
1918+
rollout_step_percentage: 10,
1919+
unsafe: {
1920+
unsafe_field: "unsafe_value",
1921+
configuration: { network: "unsafe_network_value" },
1922+
},
1923+
},
1924+
],
1925+
});
1926+
1927+
mockGetApplications([
1928+
{
1929+
id: "abc",
1930+
name: "my-container",
1931+
instances: 0,
1932+
max_instances: 10,
1933+
created_at: new Date().toString(),
1934+
version: 1,
1935+
account_id: "1",
1936+
scheduling_policy: SchedulingPolicy.DEFAULT,
1937+
configuration: {
1938+
image: "registry.cloudflare.com/some-account-id/my-container:old",
1939+
disk: {
1940+
size: "2GB",
1941+
size_mb: 2000,
1942+
},
1943+
vcpu: 0.0625,
1944+
memory: "256MB",
1945+
memory_mib: 256,
1946+
},
1947+
constraints: {
1948+
tier: 1,
1949+
},
1950+
durable_objects: {
1951+
namespace_id: "1",
1952+
},
1953+
},
1954+
]);
1955+
1956+
mockModifyApplication({
1957+
max_instances: 20,
1958+
// @ts-expect-error - testing unsafe.containers with custom fields
1959+
unsafe_field: "unsafe_value",
1960+
configuration: {
1961+
image: "registry.cloudflare.com/some-account-id/hello:world",
1962+
},
1963+
});
1964+
1965+
mockCreateApplicationRollout({
1966+
description: "Progressive update",
1967+
strategy: "rolling",
1968+
kind: "full_auto",
1969+
step_percentage: 10,
1970+
target_configuration: {
1971+
image: "registry.cloudflare.com/some-account-id/hello:world",
1972+
network: "unsafe_network_value",
1973+
},
1974+
});
1975+
1976+
await runWrangler("deploy index.js");
1977+
1978+
expect(std.err).toMatchInlineSnapshot(`""`);
1979+
});
1980+
});
1981+
18611982
// Docker mock factory
18621983
function createDockerMockChain(
18631984
containerName: string,

packages/wrangler/src/containers/deploy.ts

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ import type {
4545
} from "@cloudflare/containers-shared";
4646
import type { Config, ContainerApp } from "@cloudflare/workers-utils";
4747

48+
/**
49+
* Source overwrites target
50+
*/
4851
function mergeDeep<T>(target: T, source: Partial<T>): T {
4952
if (typeof target !== "object" || target === null) {
5053
return source as T;
@@ -238,12 +241,16 @@ export async function apply(
238241

239242
// let's always convert normalised container config -> CreateApplicationRequest
240243
// since CreateApplicationRequest is a superset of ModifyApplicationRequestBody
241-
const appConfig = containerConfigToCreateRequest(
242-
accountId,
243-
containerConfig,
244-
imageRef,
245-
args.durable_object_namespace_id,
246-
prevApp
244+
const appConfig = mergeIfUnsafe(
245+
config,
246+
containerConfigToCreateRequest(
247+
accountId,
248+
containerConfig,
249+
imageRef,
250+
args.durable_object_namespace_id,
251+
prevApp
252+
),
253+
containerConfig.name
247254
);
248255

249256
if (prevApp !== undefined && prevApp !== null) {
@@ -272,7 +279,12 @@ export async function apply(
272279
)
273280
);
274281

275-
const modifyReq = createApplicationToModifyApplication(appConfig);
282+
// this will have removed the unsafe fields, so we need to add them back in after
283+
const modifyReq = mergeIfUnsafe(
284+
config,
285+
createApplicationToModifyApplication(appConfig),
286+
appConfig.name
287+
);
276288
/** only used for diffing */
277289
const nowContainer = mergeDeep(
278290
normalisedPrevApp,
@@ -340,6 +352,7 @@ export async function apply(
340352
.forEach((el) => log(` ${el}`));
341353
newline();
342354
// add to the actions array to create the app later
355+
343356
await doAction({
344357
action: "create",
345358
application: appConfig,
@@ -349,6 +362,27 @@ export async function apply(
349362
endSection("Applied changes");
350363
}
351364

365+
/**
366+
* If there is an unsafe container config that matches this container by class_name,
367+
* merge the unsafe config into the Create/Modify request.
368+
*/
369+
function mergeIfUnsafe<
370+
T extends CreateApplicationRequest | ModifyApplicationRequestBody,
371+
>(fullConfig: Config, containerConfig: T, name: string) {
372+
const unsafeContainerConfig = fullConfig.containers?.find((original) => {
373+
return original.name === name && original.unsafe !== undefined;
374+
});
375+
376+
if (unsafeContainerConfig) {
377+
return mergeDeep<T>(
378+
containerConfig,
379+
unsafeContainerConfig.unsafe as Partial<T>
380+
);
381+
} else {
382+
return containerConfig;
383+
}
384+
}
385+
352386
export function formatError(err: ApiError): string {
353387
try {
354388
const maybeError = JSON.parse(err.body.error);

0 commit comments

Comments
 (0)