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
26 changes: 20 additions & 6 deletions __tests__/github-builder/registry-identities.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import {RegistryIdentities} from '../../src/github-builder/registry-identities.j
const aws = {type: 'aws-ecr', registry: '123.dkr.ecr.us-east-1.amazonaws.com', 'role-to-assume': 'arn:aws:iam::123:role/build', region: 'us-east-1'};
const gcp = {type: 'gcp-wif', registry: 'us-docker.pkg.dev', workload_identity_provider: 'projects/123/locations/global/workloadIdentityPools/pool/providers/provider', service_account: 'build@example.iam.gserviceaccount.com'};
const hub = {type: 'dockerhub', username: 'builder', connection_id: 'connection'};
const azure = {type: 'azure-acr', registry: 'myregistry.azurecr.io', client_id: 'client', tenant_id: 'tenant', subscription_id: 'subscription'};
const chainguard = {type: 'chainguard', identity: 'organization/identity'};
const identities = [aws, gcp, hub, azure, chainguard];

describe('RegistryIdentities.parse', () => {
it.each(['', ' \n\t', 'null', '~', '---\n', '[]'])('accepts empty configuration %j', input => {
Expand All @@ -34,29 +37,33 @@ describe('RegistryIdentities.parse', () => {
});

it('accepts all providers in a list with optional field defaults', () => {
expect(RegistryIdentities.parse(JSON.stringify([aws, gcp, hub]))).toEqual({
expect(RegistryIdentities.parse(JSON.stringify(identities))).toEqual({
awsEcr: {registry: aws.registry, roleToAssume: aws['role-to-assume'], region: aws.region},
gcpWif: {registry: gcp.registry, workloadIdentityProvider: gcp.workload_identity_provider, serviceAccount: gcp.service_account, projectId: ''},
dockerhubOidc: {registry: 'docker.io', username: 'builder', connectionID: 'connection'}
dockerhubOidc: {registry: 'docker.io', username: 'builder', connectionID: 'connection'},
azureAcr: {registry: azure.registry, clientId: azure.client_id, tenantId: azure.tenant_id, subscriptionId: azure.subscription_id},
chainguard: {identity: chainguard.identity, apkHost: 'apk.cgr.dev', librariesHost: 'libraries.cgr.dev'}
});
});

it('preserves explicit optional fields, trimming whitespace', () => {
const result = RegistryIdentities.parse(
JSON.stringify([
{...gcp, project_id: ' project '},
{...hub, registry: ' index.docker.io '}
{...hub, registry: ' index.docker.io '},
{...chainguard, apk_host: ' apk.example.com ', libraries_host: ' libraries.example.com '}
])
);
expect(result.gcpWif?.projectId).toBe('project');
expect(result.dockerhubOidc?.registry).toBe('index.docker.io');
expect(result.chainguard).toEqual({identity: chainguard.identity, apkHost: 'apk.example.com', librariesHost: 'libraries.example.com'});
});

it.each([aws, gcp, hub])('rejects duplicate provider $type', identity => {
it.each(identities)('rejects duplicate provider $type', identity => {
expect(() => RegistryIdentities.parse(JSON.stringify([identity, identity]))).toThrow(`only one ${identity.type} registry identity is supported`);
});

it.each([aws, gcp, hub])('rejects unknown fields for $type', identity => {
it.each(identities)('rejects unknown fields for $type', identity => {
expect(() => RegistryIdentities.parse(JSON.stringify({...identity, unexpected: 'value'}))).toThrow(`registry-identities[0].unexpected is not supported for ${identity.type}`);
});

Expand All @@ -69,6 +76,11 @@ describe('RegistryIdentities.parse', () => {
[gcp, 'service_account'],
[hub, 'username'],
[hub, 'connection_id'],
[azure, 'registry'],
[azure, 'client_id'],
[azure, 'tenant_id'],
[azure, 'subscription_id'],
[chainguard, 'identity'],
[hub, 'type']
] as Array<[Record<string, unknown>, string]>)('rejects missing or invalid required fields in %j: %s', (identity, key) => {
const missing = {...identity};
Expand All @@ -81,7 +93,9 @@ describe('RegistryIdentities.parse', () => {

it.each([
[gcp, 'project_id'],
[hub, 'registry']
[hub, 'registry'],
[chainguard, 'apk_host'],
[chainguard, 'libraries_host']
] as Array<[Record<string, unknown>, string]>)('rejects invalid optional fields in %j: %s', (identity, key) => {
for (const value of ['', ' ', null, false, 123, [], {}]) {
expect(() => RegistryIdentities.parse(JSON.stringify({...identity, [key]: value}))).toThrow(`registry-identities[0].${key} must be a non-empty string`);
Expand Down
34 changes: 34 additions & 0 deletions src/github-builder/registry-identities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@ export interface RegistryIdentityConfig {
username: string;
connectionID: string;
};
azureAcr?: {
registry: string;
clientId: string;
tenantId: string;
subscriptionId: string;
};
chainguard?: {
identity: string;
apkHost: string;
librariesHost: string;
};
}

// Parses github-builder keyless registry identity configuration
Expand Down Expand Up @@ -110,6 +121,29 @@ export class RegistryIdentities {
connectionID: requireString('connection_id')
};
break;
case 'azure-acr':
validateKeys(['type', 'registry', 'client_id', 'tenant_id', 'subscription_id']);
if (result.azureAcr) {
RegistryIdentities.fail('only one azure-acr registry identity is supported');
}
result.azureAcr = {
registry: requireString('registry'),
clientId: requireString('client_id'),
tenantId: requireString('tenant_id'),
subscriptionId: requireString('subscription_id')
};
break;
case 'chainguard':
validateKeys(['type', 'identity', 'apk_host', 'libraries_host']);
if (result.chainguard) {
RegistryIdentities.fail('only one chainguard registry identity is supported');
}
result.chainguard = {
identity: requireString('identity'),
apkHost: optionalString('apk_host', 'apk.cgr.dev'),
librariesHost: optionalString('libraries_host', 'libraries.cgr.dev')
};
break;
default:
RegistryIdentities.fail(`${location}.type has unsupported provider ${type}`);
}
Expand Down
Loading