@puls-atlas/cli
Version:
The Puls Atlas CLI tool for managing Atlas projects
190 lines • 9.12 kB
JavaScript
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
};