Skip to content
Merged
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
16 changes: 16 additions & 0 deletions .changeset/common-boats-help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"@cloudflare/containers-shared": minor
"wrangler": minor
---

Add Google Artifact Registry support to `containers registries configure`

`wrangler containers registries configure` now recognizes `*-docker.pkg.dev` (Google Artifact Registry) domains.

- The Google service account email is the public credential, supplied with `--gar-email`. It must match the `client_email` in the service account key.
- The service account JSON key is the private credential. It is provided via stdin (a file path, raw JSON, or base64) or an interactive prompt (a file path or base64) — never as a CLI flag, so it does not appear in shell history. The key is validated against `--gar-email` and stored base64-encoded.
- Secret reuse inherits the existence-first flow: when the target Secrets Store secret already exists, it is reused by reference and the key is not required. In that case the email cannot be verified locally; it is validated against the key when images are pulled.

```sh
<path-to-key>.json | npx wrangler@latest containers registries configure <region>-docker.pkg.dev --gar-email=<service-account-email> --secret-name=Google_Service_Account_JSON_Key
```
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@
export enum ExternalRegistryKind {
ECR = "ECR",
DOCKER_HUB = "DockerHub",
GAR = "GAR",
}
142 changes: 141 additions & 1 deletion packages/containers-shared/src/images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,8 @@ export function resolveImageName(accountId: string, image: string): string {

/**
* Get type of container registry, and validate.
* Currently we support Cloudflare managed registries and AWS ECR.
* We support Cloudflare managed registries plus the external registries listed in
* `acceptedRegistries` below (currently AWS ECR, DockerHub, and Google Artifact Registry).
* When using Cloudflare managed registries we expect CLOUDFLARE_CONTAINER_REGISTRY to be set
*/
export const getAndValidateRegistryType = (domain: string): RegistryPattern => {
Expand Down Expand Up @@ -259,6 +260,12 @@ export const getAndValidateRegistryType = (domain: string): RegistryPattern => {
name: "DockerHub",
secretType: "DockerHub PAT Token",
},
{
type: ExternalRegistryKind.GAR,
pattern: /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?-docker\.pkg\.dev$/,
name: "Google Artifact Registry",
secretType: "Google Service Account JSON Key",
},
{
type: "cloudflare",
// Make a regex based on the env var CLOUDFLARE_CONTAINER_REGISTRY
Expand Down Expand Up @@ -293,3 +300,136 @@ interface RegistryPattern {
pattern: RegExp;
name: string;
}

type ServiceAccountKey = {
private_key: string;
client_email: string;
private_key_id?: string;
};

function invalidGarCredentialError(): UserError {
return new UserError(
"The Google service account key must be a JSON key file or its base64-encoded form.",
{
telemetryMessage:
"containers registries configure invalid gar credential",
}
);
}

function tryParseJson(value: string): unknown | undefined {
try {
return JSON.parse(value) as unknown;
} catch {
return undefined;
}
}

function assertJsonObject(parsed: unknown): Record<string, unknown> {
if (typeof parsed !== "object" || parsed === null) {
throw invalidGarCredentialError();
}
return parsed as Record<string, unknown>;
}

function validateServiceAccountKey(
accountKey: Record<string, unknown>
): ServiceAccountKey {
const privateKey = accountKey.private_key;
const clientEmail = accountKey.client_email;
const rawPrivateKeyId = accountKey.private_key_id;
if (typeof privateKey !== "string" || typeof clientEmail !== "string") {
throw new UserError(
"The Google service account key is missing required fields (private_key, client_email).",
{
telemetryMessage:
"containers registries configure gar credential missing fields",
}
);
}
if (privateKey.length === 0 || clientEmail.length === 0) {
throw new UserError(
"The Google service account key has an empty private_key or client_email.",
{
telemetryMessage:
"containers registries configure gar credential empty fields",
}
);
}
let privateKeyId: string | undefined;
if (rawPrivateKeyId === undefined) {
privateKeyId = undefined;
} else if (
typeof rawPrivateKeyId === "string" &&
rawPrivateKeyId.length > 0
) {
privateKeyId = rawPrivateKeyId;
} else {
throw new UserError(
"The Google service account key has an empty or invalid private_key_id.",
{
telemetryMessage:
"containers registries configure gar credential invalid private key id",
}
);
}
return {
private_key: privateKey,
client_email: clientEmail,
private_key_id: privateKeyId,
};
}

/**
* Validates a Google service account JSON key and returns it base64-encoded for
* storage as the private credential.
*
* Accepts the raw JSON key contents or its base64-encoded form. Throws a
* `UserError` if the key is malformed, or if `expectedEmail` (the
* `--gar-email` public credential) does not match the `client_email` in the key.
*/
export function validateAndEncodeGarKey(
rawKey: string,
expectedEmail: string
): string {
const trimmed = rawKey.trim();

let base64Key: string;
let json: Record<string, unknown>;
const rawJson = tryParseJson(trimmed);
if (rawJson !== undefined) {
json = assertJsonObject(rawJson);
base64Key = Buffer.from(trimmed, "utf8").toString("base64");
} else {
if (trimmed.startsWith("-----BEGIN")) {
throw new UserError(
"The provided key appears to be a PEM private key. Provide the full Google service-account JSON key file, not just the private key.",
{
telemetryMessage:
"containers registries configure gar credential pem key",
}
);
}
base64Key = trimmed.replace(/\s+/g, "");
const decodedJson = tryParseJson(
Buffer.from(base64Key, "base64").toString("utf8")
);
if (decodedJson === undefined) {
throw invalidGarCredentialError();
}
json = assertJsonObject(decodedJson);
}

const key = validateServiceAccountKey(json);

if (key.client_email !== expectedEmail) {
throw new UserError(
`The provided --gar-email "${expectedEmail}" does not match the service account email "${key.client_email}" in the key.`,
{
telemetryMessage: "containers registries configure gar email mismatch",
}
);
}

return base64Key;
}
Loading
Loading