@@ -226,7 +226,8 @@ export function resolveImageName(accountId: string, image: string): string {
226226
227227/**
228228 * Get type of container registry, and validate.
229- * Currently we support Cloudflare managed registries and AWS ECR.
229+ * We support Cloudflare managed registries plus the external registries listed in
230+ * `acceptedRegistries` below (currently AWS ECR, DockerHub, and Google Artifact Registry).
230231 * When using Cloudflare managed registries we expect CLOUDFLARE_CONTAINER_REGISTRY to be set
231232 */
232233export const getAndValidateRegistryType = ( domain : string ) : RegistryPattern => {
@@ -259,6 +260,12 @@ export const getAndValidateRegistryType = (domain: string): RegistryPattern => {
259260 name : "DockerHub" ,
260261 secretType : "DockerHub PAT Token" ,
261262 } ,
263+ {
264+ type : ExternalRegistryKind . GAR ,
265+ pattern : / ^ [ a - z 0 - 9 ] (?: [ a - z 0 - 9 - ] * [ a - z 0 - 9 ] ) ? - d o c k e r \. p k g \. d e v $ / ,
266+ name : "Google Artifact Registry" ,
267+ secretType : "Google Service Account JSON Key" ,
268+ } ,
262269 {
263270 type : "cloudflare" ,
264271 // Make a regex based on the env var CLOUDFLARE_CONTAINER_REGISTRY
@@ -293,3 +300,136 @@ interface RegistryPattern {
293300 pattern : RegExp ;
294301 name : string ;
295302}
303+
304+ type ServiceAccountKey = {
305+ private_key : string ;
306+ client_email : string ;
307+ private_key_id ?: string ;
308+ } ;
309+
310+ function invalidGarCredentialError ( ) : UserError {
311+ return new UserError (
312+ "The Google service account key must be a JSON key file or its base64-encoded form." ,
313+ {
314+ telemetryMessage :
315+ "containers registries configure invalid gar credential" ,
316+ }
317+ ) ;
318+ }
319+
320+ function tryParseJson ( value : string ) : unknown | undefined {
321+ try {
322+ return JSON . parse ( value ) as unknown ;
323+ } catch {
324+ return undefined ;
325+ }
326+ }
327+
328+ function assertJsonObject ( parsed : unknown ) : Record < string , unknown > {
329+ if ( typeof parsed !== "object" || parsed === null ) {
330+ throw invalidGarCredentialError ( ) ;
331+ }
332+ return parsed as Record < string , unknown > ;
333+ }
334+
335+ function validateServiceAccountKey (
336+ accountKey : Record < string , unknown >
337+ ) : ServiceAccountKey {
338+ const privateKey = accountKey . private_key ;
339+ const clientEmail = accountKey . client_email ;
340+ const rawPrivateKeyId = accountKey . private_key_id ;
341+ if ( typeof privateKey !== "string" || typeof clientEmail !== "string" ) {
342+ throw new UserError (
343+ "The Google service account key is missing required fields (private_key, client_email)." ,
344+ {
345+ telemetryMessage :
346+ "containers registries configure gar credential missing fields" ,
347+ }
348+ ) ;
349+ }
350+ if ( privateKey . length === 0 || clientEmail . length === 0 ) {
351+ throw new UserError (
352+ "The Google service account key has an empty private_key or client_email." ,
353+ {
354+ telemetryMessage :
355+ "containers registries configure gar credential empty fields" ,
356+ }
357+ ) ;
358+ }
359+ let privateKeyId : string | undefined ;
360+ if ( rawPrivateKeyId === undefined ) {
361+ privateKeyId = undefined ;
362+ } else if (
363+ typeof rawPrivateKeyId === "string" &&
364+ rawPrivateKeyId . length > 0
365+ ) {
366+ privateKeyId = rawPrivateKeyId ;
367+ } else {
368+ throw new UserError (
369+ "The Google service account key has an empty or invalid private_key_id." ,
370+ {
371+ telemetryMessage :
372+ "containers registries configure gar credential invalid private key id" ,
373+ }
374+ ) ;
375+ }
376+ return {
377+ private_key : privateKey ,
378+ client_email : clientEmail ,
379+ private_key_id : privateKeyId ,
380+ } ;
381+ }
382+
383+ /**
384+ * Validates a Google service account JSON key and returns it base64-encoded for
385+ * storage as the private credential.
386+ *
387+ * Accepts the raw JSON key contents or its base64-encoded form. Throws a
388+ * `UserError` if the key is malformed, or if `expectedEmail` (the
389+ * `--gar-email` public credential) does not match the `client_email` in the key.
390+ */
391+ export function validateAndEncodeGarKey (
392+ rawKey : string ,
393+ expectedEmail : string
394+ ) : string {
395+ const trimmed = rawKey . trim ( ) ;
396+
397+ let base64Key : string ;
398+ let json : Record < string , unknown > ;
399+ const rawJson = tryParseJson ( trimmed ) ;
400+ if ( rawJson !== undefined ) {
401+ json = assertJsonObject ( rawJson ) ;
402+ base64Key = Buffer . from ( trimmed , "utf8" ) . toString ( "base64" ) ;
403+ } else {
404+ if ( trimmed . startsWith ( "-----BEGIN" ) ) {
405+ throw new UserError (
406+ "The provided key appears to be a PEM private key. Provide the full Google service-account JSON key file, not just the private key." ,
407+ {
408+ telemetryMessage :
409+ "containers registries configure gar credential pem key" ,
410+ }
411+ ) ;
412+ }
413+ base64Key = trimmed . replace ( / \s + / g, "" ) ;
414+ const decodedJson = tryParseJson (
415+ Buffer . from ( base64Key , "base64" ) . toString ( "utf8" )
416+ ) ;
417+ if ( decodedJson === undefined ) {
418+ throw invalidGarCredentialError ( ) ;
419+ }
420+ json = assertJsonObject ( decodedJson ) ;
421+ }
422+
423+ const key = validateServiceAccountKey ( json ) ;
424+
425+ if ( key . client_email !== expectedEmail ) {
426+ throw new UserError (
427+ `The provided --gar-email "${ expectedEmail } " does not match the service account email "${ key . client_email } " in the key.` ,
428+ {
429+ telemetryMessage : "containers registries configure gar email mismatch" ,
430+ }
431+ ) ;
432+ }
433+
434+ return base64Key ;
435+ }
0 commit comments