UNPKG

@puls-atlas/cli

Version:

The Puls Atlas CLI tool for managing Atlas projects

190 lines 9.12 kB
import { resolveSearchCloudRunDeployConfig } from './planning.js'; import { parseGcloudJsonOutput, runGcloudFileCommand } from '../../utils/gcloud.js'; import { normalizeOptionalString, spreadIf } from '../../utils/value.js'; const ARTIFACT_REGISTRY_HOST_SUFFIX = '-docker.pkg.dev'; const ARTIFACT_REGISTRY_READER_ROLE = 'roles/artifactregistry.reader'; const createRepositoryUri = reference => `${reference.location}-docker.pkg.dev/${reference.projectId}/${reference.repository}`; const createImageRepositoryKey = reference => `${reference.location}/${reference.projectId}/${reference.repository}`; const createParsedArtifactRegistryImageReference = ({ location, projectId, repository, tag = null, digest = null, ...rest }) => ({ tag, digest, location, projectId, repository, repositoryUri: createRepositoryUri({ location, projectId, repository }), ...rest }); const createArtifactRegistryAccessResult = (reference, serviceAgentEmail, status) => ({ repository: reference.repository, repositoryUri: createRepositoryUri(reference), role: ARTIFACT_REGISTRY_READER_ROLE, serviceAgentEmail, status }); export const parseArtifactRegistryDockerImageReference = imageReference => { const normalizedImageReference = normalizeOptionalString(imageReference); if (!normalizedImageReference) { return null; } const [host, ...pathSegments] = normalizedImageReference.split('/'); if (!host || !host.endsWith(ARTIFACT_REGISTRY_HOST_SUFFIX) || pathSegments.length < 3) { return null; } const projectId = pathSegments[0]; const repository = pathSegments[1]; const imagePathWithRef = pathSegments.slice(2).join('/'); const digestSeparatorIndex = imagePathWithRef.lastIndexOf('@'); const tagSeparatorIndex = imagePathWithRef.lastIndexOf(':'); const location = host.slice(0, -ARTIFACT_REGISTRY_HOST_SUFFIX.length); if (digestSeparatorIndex > -1) { const imageName = imagePathWithRef.slice(0, digestSeparatorIndex); const digest = imagePathWithRef.slice(digestSeparatorIndex + 1); return createParsedArtifactRegistryImageReference({ digest, host, imageName, imageReference: normalizedImageReference, location, projectId, repository, tag: null }); } if (tagSeparatorIndex < 0) { return null; } const imageName = imagePathWithRef.slice(0, tagSeparatorIndex); const tag = imagePathWithRef.slice(tagSeparatorIndex + 1); if (!imageName || !tag) { return null; } return createParsedArtifactRegistryImageReference({ host, imageName, imageReference: normalizedImageReference, location, projectId, repository, tag }); }; export const createDigestPinnedArtifactRegistryImageReference = (reference, digest) => `${reference.location}-docker.pkg.dev/${reference.projectId}/${reference.repository}/${reference.imageName}@${digest}`; export const createCloudRunServiceAgentEmail = projectNumber => { const normalizedProjectNumber = normalizeOptionalString(projectNumber); if (!normalizedProjectNumber) { throw new Error('Cloud Run service agent resolution requires a project number.'); } return `service-${normalizedProjectNumber}@serverless-robot-prod.iam.gserviceaccount.com`; }; export const resolveGoogleCloudProjectNumber = (projectId, options = {}) => { const projectNumber = runGcloudFileCommand(['projects', 'describe', projectId, '--format="value(projectNumber)"'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }, options.runCommand); const normalizedProjectNumber = normalizeOptionalString(projectNumber); if (!normalizedProjectNumber) { throw new Error(`Could not resolve the Google Cloud project number for ${projectId}.`); } return normalizedProjectNumber; }; const getArtifactRegistryRepositoryIamPolicy = (reference, options = {}) => { const policy = runGcloudFileCommand(['artifacts', 'repositories', 'get-iam-policy', reference.repository, `--location=${reference.location}`, `--project=${reference.projectId}`, '--format=json'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }, options.runCommand); return parseGcloudJsonOutput(policy, `Artifact Registry IAM policy for repository ${reference.repository}`); }; export const hasArtifactRegistryReaderBinding = (policy, serviceAgentEmail) => { const member = `serviceAccount:${serviceAgentEmail}`; return (policy?.bindings ?? []).some(binding => binding?.role === ARTIFACT_REGISTRY_READER_ROLE && Array.isArray(binding.members) && binding.members.includes(member)); }; export const ensureArtifactRegistryReaderBinding = (reference, serviceAgentEmail, options = {}) => { const policy = getArtifactRegistryRepositoryIamPolicy(reference, options); if (hasArtifactRegistryReaderBinding(policy, serviceAgentEmail)) { return createArtifactRegistryAccessResult(reference, serviceAgentEmail, 'existing'); } if (options.dryRun === true) { return createArtifactRegistryAccessResult(reference, serviceAgentEmail, 'would-create'); } runGcloudFileCommand(['artifacts', 'repositories', 'add-iam-policy-binding', reference.repository, `--location=${reference.location}`, `--project=${reference.projectId}`, `--member=serviceAccount:${serviceAgentEmail}`, `--role=${ARTIFACT_REGISTRY_READER_ROLE}`], { stdio: 'inherit' }, options.runCommand); return createArtifactRegistryAccessResult(reference, serviceAgentEmail, 'created'); }; export const resolveArtifactRegistryDockerImageDigest = (imageReference, options = {}) => { const parsedReference = parseArtifactRegistryDockerImageReference(imageReference); if (!parsedReference) { throw new Error(`Atlas search runtime image ${imageReference} is not a supported Artifact Registry Docker reference.`); } if (parsedReference.digest) { return parsedReference.digest; } const digest = runGcloudFileCommand(['artifacts', 'docker', 'images', 'describe', parsedReference.imageReference, `--project=${parsedReference.projectId}`, '--format="value(image_summary.digest)"'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }, options.runCommand); const normalizedDigest = normalizeOptionalString(digest); if (!normalizedDigest) { throw new Error(`Could not resolve an immutable digest for Atlas search runtime image ${parsedReference.imageReference}.`); } return normalizedDigest; }; export const prepareSearchRuntimeRegistryAccess = (context, options = {}) => { const cloudRunConfig = resolveSearchCloudRunDeployConfig(context); const originalImages = { job: cloudRunConfig.jobImage, service: cloudRunConfig.serviceImage, ...spreadIf(cloudRunConfig.sourceRunnerJobImage, { sourceRunnerJob: cloudRunConfig.sourceRunnerJobImage }), syncService: cloudRunConfig.syncServiceImage }; const parsedImageEntries = Object.entries(originalImages).map(([name, imageReference]) => [name, parseArtifactRegistryDockerImageReference(imageReference)]); const crossProjectRepositories = [...new Map(parsedImageEntries.map(([, parsedReference]) => parsedReference).filter(parsedReference => parsedReference !== null && parsedReference.projectId !== context.projectId).map(parsedReference => [createImageRepositoryKey(parsedReference), parsedReference])).values()]; const consumerProjectNumber = crossProjectRepositories.length > 0 ? resolveGoogleCloudProjectNumber(context.projectId, options) : null; const serviceAgentEmail = consumerProjectNumber ? createCloudRunServiceAgentEmail(consumerProjectNumber) : null; const repositoryAccess = serviceAgentEmail === null ? [] : crossProjectRepositories.map(parsedReference => ensureArtifactRegistryReaderBinding(parsedReference, serviceAgentEmail, options)); const digestCache = new Map(); const images = Object.fromEntries(parsedImageEntries.map(([name, parsedReference]) => { if (!parsedReference) { return [name, originalImages[name]]; } if (parsedReference.digest) { return [name, parsedReference.imageReference]; } if (!digestCache.has(parsedReference.imageReference)) { digestCache.set(parsedReference.imageReference, resolveArtifactRegistryDockerImageDigest(parsedReference.imageReference, options)); } return [name, createDigestPinnedArtifactRegistryImageReference(parsedReference, digestCache.get(parsedReference.imageReference))]; })); const warnings = repositoryAccess.filter(result => result.status === 'would-create').map(result => `Dry run: would grant ${result.role} on ${result.repositoryUri} to ${result.serviceAgentEmail} ` + 'so Cloud Run can import Atlas runtime images from the shared registry.'); return { consumerProjectNumber, images, originalImages, repositoryAccess, serviceAgentEmail, warnings }; }; export default { createCloudRunServiceAgentEmail, createDigestPinnedArtifactRegistryImageReference, ensureArtifactRegistryReaderBinding, hasArtifactRegistryReaderBinding, parseArtifactRegistryDockerImageReference, prepareSearchRuntimeRegistryAccess, resolveArtifactRegistryDockerImageDigest, resolveGoogleCloudProjectNumber };