Skip to content
Open
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
18 changes: 18 additions & 0 deletions .changeset/open-donkeys-behave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"@cloudflare/vite-plugin": minor
"@cloudflare/containers-shared": minor
"wrangler": minor
---

Containers: Allow users to directly authenticate external image registries in local dev

Previously, we always queried the API for stored registry credentials and used those to pull images.
This means that if you are using an external registry (ECR, dockerhub) then you have to configure
registry credentials remotely before running local dev. Also, it meant that the Vite-plugin didn't
work with external image registries, since the plugin could not call the containers API.

Now you can directly authenticate with your external registry provider (using `docker login` etc.),
and Wrangler or Vite will be able to pull the image specified in the `contaienrs.image` field in your
config file.

The Cloudflare-managed registry (registry.cloudflare.com) currently still does not work with the Vite plugin.
4 changes: 2 additions & 2 deletions packages/containers-shared/src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import type {
BuildArgs,
ContainerDevOptions,
ImageURIConfig,
Logger,
WranglerLogger,
} from "./types";

export async function constructBuildCommand(
options: BuildArgs,
logger?: Logger
logger?: WranglerLogger
) {
const platform = options.platform ?? "linux/amd64";
const buildCmd = [
Expand Down
4 changes: 2 additions & 2 deletions packages/containers-shared/src/client/core/OpenAPI.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* istanbul ignore file */
/* tslint:disable */
import type { Logger } from "../../types";
import type { WranglerLogger } from "../../types";
import type { ApiRequestOptions } from "./ApiRequestOptions";

type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
Expand All @@ -16,7 +16,7 @@ export type OpenAPIConfig = {
PASSWORD?: string | Resolver<string>;
HEADERS?: Headers | Resolver<Headers>;
ENCODE_PATH?: (path: string) => string;
LOGGER?: Logger | undefined;
LOGGER?: WranglerLogger | undefined;
};

export const OpenAPI: OpenAPIConfig = {
Expand Down
49 changes: 44 additions & 5 deletions packages/containers-shared/src/images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,46 @@ import {
runDockerCmd,
verifyDockerInstalled,
} from "./utils";
import type { ContainerDevOptions, DockerfileConfig } from "./types";
import type {
ContainerDevOptions,
DockerfileConfig,
ViteLogger,
WranglerLogger,
} from "./types";

export async function pullImage(
dockerPath: string,
options: Exclude<ContainerDevOptions, DockerfileConfig>
options: Exclude<ContainerDevOptions, DockerfileConfig>,
logger: WranglerLogger | ViteLogger,
isVite: boolean
): Promise<{ abort: () => void; ready: Promise<void> }> {
const domain = new URL(`http://${options.image_uri}`).hostname;
await dockerLoginImageRegistry(dockerPath, domain);

const isExternalRegistry = domain !== getCloudflareContainerRegistry();
try {
// this will fail in two cases:
// 1. this is being called from the vite plugin (doesn't have the appropriate auth context)
// 2.
Copy link
Member

Choose a reason for hiding this comment

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

domain not configured I think?

And if so, are we okay with local dev working for something that won't work in prod?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

whoops yes i forgot to finish that sentence. :') but yes exactly.

I think this is equivalent to being able to local dev with kv etc. but not have created the kv yet. So that wouldn't work at deploy, but with kv etc. we do have a nice flow that provisions KV for you at deploy time.

What we could do is check if you can get the creds at deploy time, and if you can't then run the user through the registries configure flow.

Alternatively, I could make the fall back only happen for vite.

Copy link
Member

Choose a reason for hiding this comment

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

What we could do is check if you can get the creds at deploy time, and if you can't then run the user through the registries configure flow.
I like this idea, feels like it would be useful for more than just local dev -> deploy workflow

Copy link
Member

Choose a reason for hiding this comment

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

okay I was thinking about this more and I'm not sure this would work with public images. especially with docker, we have no way to differentiate public and private images from domain

await dockerLoginImageRegistry(dockerPath, domain);
} catch (e) {
if (!isExternalRegistry) {
// horrible hack to check if this is from vite - vite logger doesn't have debug method
if (isVite) {
throw new UserError(
`Using images from the Cloudflare-managed registry is not currently supported with the Vite plugin.\n` +
`You should set \`containers.image\` to a Dockerfile or use a supported external registry and authenticate to that registry separately using \`docker login\` or similar.\n` +
`Supported external registries are currently: ${Object.values(ExternalRegistryKind).join(", ")}.`
);
}
throw e;
}
logger?.warn(
"Unable to retrieve configured registry credentials from Cloudflare." +
"\nYou will need to run `wrangler containers registries configure` before deploying." +
"\nAttempting to pull image anyway, in case you have authenticated this registry separately..."
);
}

const pull = runDockerCmd(dockerPath, [
"pull",
options.image_uri,
Expand Down Expand Up @@ -64,7 +96,9 @@ export async function prepareContainerImagesForDev(args: {
onContainerImagePreparationEnd: (args: {
containerOptions: ContainerDevOptions;
}) => void;
}) {
logger: WranglerLogger | ViteLogger;
isVite: boolean;
}): Promise<void> {
const {
dockerPath,
containerOptions,
Expand Down Expand Up @@ -94,7 +128,12 @@ export async function prepareContainerImagesForDev(args: {
containerOptions: options,
});
} else {
const pull = await pullImage(dockerPath, options);
const pull = await pullImage(
dockerPath,
options,
args.logger,
args.isVite
);
onContainerImagePreparationStart({
containerOptions: options,
abort: () => {
Expand Down
7 changes: 6 additions & 1 deletion packages/containers-shared/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,19 @@ import type {
} from "./client";
import type { ApplicationAffinityHardwareGeneration } from "./client/models/ApplicationAffinityHardwareGeneration";

export interface Logger {
export interface WranglerLogger {
debug: (...args: unknown[]) => void;
debugWithSanitization: (label: string, ...args: unknown[]) => void;
log: (...args: unknown[]) => void;
info: (...args: unknown[]) => void;
warn: (...args: unknown[]) => void;
error: (...args: unknown[]) => void;
}
export interface ViteLogger {
info: (msg: string) => void;
warn: (msg: string) => void;
error: (msg: string) => void;
}

export type BuildArgs = {
/** image tag in the format `name:tag`, where tag is optional */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"compatibility_date": "2025-04-03",
"containers": [
{
"image": "./Dockerfile",
"image": "registry.cloudflare.com/8d783f274e1f82dc46744c297b015a2f/ci-container-dont-delete:latest",
"class_name": "Container",
"name": "http2",
"max_instances": 2,
Expand Down
2 changes: 2 additions & 0 deletions packages/vite-plugin-cloudflare/src/plugins/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ export const devPlugin = createPlugin("dev", (ctx) => {
containerOptions: [...containerTagToOptionsMap.values()],
onContainerImagePreparationStart: () => {},
onContainerImagePreparationEnd: () => {},
logger: viteDevServer.config.logger,
isVite: true,
});

containerImageTags = new Set(containerTagToOptionsMap.keys());
Expand Down
2 changes: 2 additions & 0 deletions packages/vite-plugin-cloudflare/src/plugins/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ export const previewPlugin = createPlugin("preview", (ctx) => {
containerOptions: [...containerTagToOptionsMap.values()],
onContainerImagePreparationStart: () => {},
onContainerImagePreparationEnd: () => {},
logger: vitePreviewServer.config.logger,
isVite: true,
});

const containerImageTags = new Set(containerTagToOptionsMap.keys());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,8 @@ export class LocalRuntimeController extends RuntimeController {
onContainerImagePreparationEnd: () => {
this.containerBeingBuilt = undefined;
},
logger: logger,
isVite: false,
});
if (this.containerBeingBuilt) {
this.containerBeingBuilt.abortRequested = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ export class MultiworkerRuntimeController extends LocalRuntimeController {
onContainerImagePreparationEnd: () => {
this.containerBeingBuilt = undefined;
},
logger: logger,
isVite: false,
});
if (this.containerBeingBuilt) {
this.containerBeingBuilt.abortRequested = false;
Expand Down
Loading