Skip to content
Draft
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
8 changes: 8 additions & 0 deletions .changeset/fuse-privileged-containers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"wrangler": minor
"miniflare": minor
---

Add `dev.privileged_containers` config and `--privileged-containers` CLI flag for FUSE in local dev

When set, miniflare launches local containers with the elevated permissions FUSE requires (`CAP_SYS_ADMIN`, `/dev/fuse`, AppArmor unconfined). Off by default.
3 changes: 3 additions & 0 deletions packages/miniflare/src/plugins/do/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import type {
// Options for a container attached to the DO
export const DOContainerOptionsSchema = z.object({
imageName: z.string(),
// When true, workerd creates the container with the elevated privileges
// needed for FUSE (CAP_SYS_ADMIN, /dev/fuse, AppArmor unconfined).
allowPrivileged: z.boolean().optional(),
});
export type DOContainerOptions = z.infer<typeof DOContainerOptionsSchema>;

Expand Down
6 changes: 6 additions & 0 deletions packages/miniflare/src/runtime/config/workerd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,14 @@ export type Worker_DurableObjectNamespace = {
className?: string;
preventEviction?: boolean;
enableSql?: boolean;
container?: Worker_ContainerOptions;
} & ({ uniqueKey?: string } | { ephemeralLocal?: Void });

export type Worker_ContainerOptions = {
imageName?: string;
allowPrivileged?: boolean;
};

export type ExternalServer = { address?: string } & (
| { http: HttpOptions }
| { https: ExternalServer_Https }
Expand Down
10 changes: 10 additions & 0 deletions packages/workers-utils/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,15 @@ export interface DevConfig {
*/
enable_containers: boolean;

/**
* When developing, whether to launch containers with the elevated privileges
* required for FUSE mounts (CAP_SYS_ADMIN, /dev/fuse, AppArmor unconfined).
* Off by default — only enable when your container needs FUSE locally.
*
* @default false
*/
privileged_containers: boolean;

/**
* Either the Docker unix socket i.e. `unix:///var/run/docker.sock` or a full configuration.
* Note that windows is only supported via WSL at the moment
Expand Down Expand Up @@ -312,6 +321,7 @@ export const defaultWranglerConfig: Config = {
host: undefined,
// Note this one is also workers only
enable_containers: true,
privileged_containers: false,
container_engine: undefined,
generate_types: false,
},
Expand Down
16 changes: 16 additions & 0 deletions packages/workers-utils/src/config/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ export type NormalizeAndValidateConfigArgs = {
upstreamProtocol?: string;
script?: string;
enableContainers?: boolean;
privilegedContainers?: boolean;
generateTypes?: boolean;
};

Expand Down Expand Up @@ -692,6 +693,7 @@ function normalizeAndValidateDev(
upstreamProtocol: upstreamProtocolArg,
remote: remoteArg,
enableContainers: enableContainersArg,
privilegedContainers: privilegedContainersArg,
generateTypes: generateTypesArg,
} = args;
assert(
Expand All @@ -709,6 +711,10 @@ function normalizeAndValidateDev(
enableContainersArg === undefined ||
typeof enableContainersArg === "boolean"
);
assert(
privilegedContainersArg === undefined ||
typeof privilegedContainersArg === "boolean"
);
assert(
generateTypesArg === undefined || typeof generateTypesArg === "boolean"
);
Expand All @@ -731,6 +737,7 @@ function normalizeAndValidateDev(
: local_protocol,
host,
enable_containers = enableContainersArg ?? true,
privileged_containers = privilegedContainersArg ?? false,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I just realised that this means that the Wrangler config value will override any command line arg, right?

Is that what you want? Classically the ordering tends to be command line arg > env var > config value > default.

I appreciate that this is how many of the other properties are being computed too. Although notably port is validated "correctly". (It's default is set later

port:
input.dev?.server?.port ??
config.dev.port ??
(await getLocalPort(initialIpListenCheck)),
)

container_engine,
generate_types = generateTypesArg ?? false,
...rest
Expand Down Expand Up @@ -778,6 +785,14 @@ function normalizeAndValidateDev(
"boolean"
);

validateOptionalProperty(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've just realised that the way that normalizeAndValidateDev() is structured, all these calls to validateOptionaProperty() are pointless, since we have already forced the values not to be undefined in the assignment from rawDev above. So these could just have been validateRequiredProperty().

Anyway, nothing to do with this PR.

diagnostics,
"dev",
"privileged_containers",
privileged_containers,
"boolean"
);

validateOptionalProperty(
diagnostics,
"dev",
Expand All @@ -803,6 +818,7 @@ function normalizeAndValidateDev(
upstream_protocol,
host,
enable_containers,
privileged_containers,
container_engine,
generate_types,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ describe("validatePagesConfig()", () => {
upstream_protocol: "https",
host: "test-host",
enable_containers: false,
privileged_containers: false,
container_engine: undefined,
generate_types: false,
},
Expand Down
38 changes: 38 additions & 0 deletions packages/wrangler/src/__tests__/dev.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1679,6 +1679,44 @@ describe.sequential("wrangler dev", () => {
});
});

describe("privileged containers", () => {
it("should default to false", async ({ expect }) => {
writeWranglerConfig({
main: "index.js",
});
fs.writeFileSync("index.js", `export default {};`);
const config = await runWranglerUntilConfig("dev");
expect(config.dev.privilegedContainers).toBe(false);
});

it("should be true when `dev.privileged_containers` is set in the Wrangler config", async ({
expect,
}) => {
writeWranglerConfig({
main: "index.js",
dev: {
privileged_containers: true,
},
});
fs.writeFileSync("index.js", `export default {};`);
const config = await runWranglerUntilConfig("dev");
expect(config.dev.privilegedContainers).toBe(true);
});

it("should be true when `--privileged-containers` is passed on the CLI", async ({
expect,
}) => {
writeWranglerConfig({
main: "index.js",
});
fs.writeFileSync("index.js", `export default {};`);
const config = await runWranglerUntilConfig(
"dev --privileged-containers"
);
expect(config.dev.privilegedContainers).toBe(true);
});
});

describe("durable_objects", () => {
it("should warn if there are remote Durable Objects, or missing migrations for local Durable Objects", async ({
expect,
Expand Down
1 change: 1 addition & 0 deletions packages/wrangler/src/api/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ export async function unstable_dev(
d1Databases,
disableDevRegistry,
testScheduled: testScheduled ?? false,
privilegedContainers: false,
enablePagesAssetsServiceBinding,
forceLocal,
liveReload,
Expand Down
4 changes: 4 additions & 0 deletions packages/wrangler/src/api/integrations/platform/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,8 @@ async function getMiniflareOptionsFromConfig(args: {
),
containerBuildId: undefined,
enableContainers: config.dev.enable_containers,
privilegedContainers:
config.dev.privileged_containers,
},
remoteProxyConnectionString
);
Expand Down Expand Up @@ -467,6 +469,8 @@ export function unstable_getMiniflareWorkerOptions(
containerDOClassNames,
containerBuildId: options?.containerBuildId,
enableContainers,
privilegedContainers:
config.dev.privileged_containers,
},
options?.remoteProxyConnectionString
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ async function resolveDevConfig(
inferOriginFromRoutes: input.dev?.inferOriginFromRoutes ?? true,
enableContainers:
input.dev?.enableContainers ?? config.dev.enable_containers,
privilegedContainers:
input.dev?.privilegedContainers ??
config.dev.privileged_containers,
dockerPath: input.dev?.dockerPath ?? getDockerPath(),
containerEngine: useContainers
? (input.dev?.containerEngine ??
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,8 @@ export async function convertToConfigBundle(
containerBuildId: event.config.dev?.containerBuildId,
containerEngine: event.config.dev.containerEngine,
enableContainers: event.config.dev.enableContainers ?? true,
privilegedContainers:
event.config.dev.privilegedContainers ?? false,
zone: getZoneForCfWorkerHeader(event.config),
sendMetrics: event.config.sendMetrics,
publicUrl: event.config.dev?.server?.port
Expand Down
6 changes: 6 additions & 0 deletions packages/wrangler/src/api/startDevWorker/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,12 @@ export interface StartDevWorkerInput {
/** Whether to build and connect to containers during local dev. Requires Docker daemon to be running. Defaults to true. */
enableContainers?: boolean;

/**
* Whether to launch local containers with the elevated privileges needed for FUSE
* (CAP_SYS_ADMIN, /dev/fuse, AppArmor unconfined). Defaults to false.
*/
privilegedContainers?: boolean;

/** Path to the dev registry directory */
registry?: string;

Expand Down
6 changes: 6 additions & 0 deletions packages/wrangler/src/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@ export const dev = createCommand({
describe: "Whether to build and enable containers during development",
hidden: true,
},
"privileged-containers": {
type: "boolean",
describe:
"Launch local containers with the elevated privileges required for FUSE (CAP_SYS_ADMIN, /dev/fuse, AppArmor unconfined)",
hidden: true,
},
site: {
describe: "Root folder of static assets for Workers Sites",
type: "string",
Expand Down
7 changes: 7 additions & 0 deletions packages/wrangler/src/dev/miniflare/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export interface ConfigBundle {
containerBuildId: string | undefined;
containerEngine: ContainerEngine | undefined;
enableContainers: boolean;
privilegedContainers: boolean;
// Zone to use for the CF-Worker header in outbound fetches
zone: string | undefined;
sendMetrics: boolean | undefined;
Expand Down Expand Up @@ -478,6 +479,7 @@ type MiniflareBindingsConfig = Pick<
| "containerDOClassNames"
| "containerBuildId"
| "enableContainers"
| "privilegedContainers"
> &
Partial<
Pick<ConfigBundle, "format" | "bundle" | "assets" | "compatibilityFlags">
Expand Down Expand Up @@ -734,6 +736,7 @@ export function buildMiniflareBindingOptions(
doClassName: className,
containerDOClassNames: config.containerDOClassNames,
containerBuildId: config.containerBuildId,
allowPrivileged: config.privilegedContainers,
})
: undefined,
});
Expand Down Expand Up @@ -1007,6 +1010,8 @@ export function buildMiniflareBindingOptions(
doClassName: className,
containerDOClassNames: config.containerDOClassNames,
containerBuildId: config.containerBuildId,
allowPrivileged:
config.privilegedContainers,
})
: undefined,
},
Expand Down Expand Up @@ -1161,6 +1166,7 @@ export function getImageNameFromDOClassName(options: {
doClassName: string;
containerDOClassNames: Set<string>;
containerBuildId: string | undefined;
allowPrivileged: boolean;
}): DOContainerOptions | undefined {
assert(
options.containerBuildId,
Expand All @@ -1173,6 +1179,7 @@ export function getImageNameFromDOClassName(options: {
options.doClassName,
options.containerBuildId
),
allowPrivileged: options.allowPrivileged,
};
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/wrangler/src/dev/start-dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ async function setupDevEnv(
registry: args.disableDevRegistry ? undefined : getRegistryPath(),
multiworkerPrimary: args.multiworkerPrimary,
enableContainers: args.enableContainers,
privilegedContainers: args.privilegedContainers,
dockerPath: args.dockerPath,
// initialise with a random id
containerBuildId: generateContainerBuildId(),
Expand Down
1 change: 1 addition & 0 deletions packages/wrangler/src/pages/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1034,6 +1034,7 @@ export const pagesDevCommand = createCommand({
siteInclude: undefined,
siteExclude: undefined,
enableContainers: false,
privilegedContainers: false,
types: false,
tunnel: undefined,
})
Expand Down