UNPKG

@puls-atlas/cli

Version:

The Puls Atlas CLI tool for managing Atlas projects

116 lines 5.62 kB
import { execFileSync as nodeExecFileSync } from 'child_process'; import { isString } from 'es-toolkit/compat'; import { runGcloudFileCommand } from './gcloud.js'; export const DEFAULT_ATLAS_ARTIFACT_BUCKET_PREFIX = 'puls-atlas-artifacts'; export const DEFAULT_ATLAS_DLQ_BUCKET_PREFIX = 'puls-atlas-dlq'; export const DEFAULT_ATLAS_ARTIFACT_BUCKET_LOCATION = 'europe-west1'; export const DEFAULT_ATLAS_DLQ_BUCKET_RETENTION_DAYS = 365; const getBucketInspectionErrorText = error => { const stderr = error?.stderr?.toString?.() ?? ''; return [error?.message, stderr].filter(Boolean).join('\n'); }; const isMissingBucketError = error => /404|not found|does not exist|bucket.*could not be found/i.test(getBucketInspectionErrorText(error)); const normalizeProjectIdSegment = (projectId, { bucketPrefix = DEFAULT_ATLAS_ARTIFACT_BUCKET_PREFIX, errorPrefix = 'Atlas artifact bucket' } = {}) => { if (!isString(projectId) || projectId.trim().length === 0) { throw new Error(`${errorPrefix} resolution requires a valid project id.`); } const maxSegmentLength = 63 - `${bucketPrefix}-`.length; const normalizedSegment = projectId.trim().toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-{2,}/g, '-').replace(/^-+|-+$/g, '').slice(0, maxSegmentLength).replace(/-+$/g, ''); if (normalizedSegment.length === 0) { throw new Error(`${errorPrefix} could not derive a valid bucket suffix from project id ${projectId}.`); } return normalizedSegment; }; const runBucketCommand = (args, runCommand) => runGcloudFileCommand(args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }, runCommand); export const resolveAtlasArtifactBucketName = projectId => `${DEFAULT_ATLAS_ARTIFACT_BUCKET_PREFIX}-${normalizeProjectIdSegment(projectId)}`; export const resolveAtlasDlqBucketName = projectId => `${DEFAULT_ATLAS_DLQ_BUCKET_PREFIX}-${normalizeProjectIdSegment(projectId, { bucketPrefix: DEFAULT_ATLAS_DLQ_BUCKET_PREFIX, errorPrefix: 'Atlas DLQ bucket' })}`; export const resolveAtlasArtifactUri = (projectId, objectPath) => { if (!isString(objectPath) || objectPath.trim().length === 0) { throw new Error('Atlas artifact URI resolution requires a non-empty object path.'); } return `gs://${resolveAtlasArtifactBucketName(projectId)}/${objectPath.trim().replace(/^\/+/, '')}`; }; export const resolveAtlasDlqUri = (projectId, objectPath) => { if (!isString(objectPath) || objectPath.trim().length === 0) { throw new Error('Atlas DLQ URI resolution requires a non-empty object path.'); } return `gs://${resolveAtlasDlqBucketName(projectId)}/${objectPath.trim().replace(/^\/+/, '')}`; }; const normalizeArtifactPathSegment = segment => { if (!isString(segment) || segment.trim().length === 0) { throw new Error('Atlas artifact path segments must be non-empty strings.'); } return segment.trim().replace(/^\/+|\/+$/g, '').replace(/\/{2,}/g, '/'); }; export const buildAtlasArtifactObjectPath = (namespace, ...pathSegments) => [namespace, ...pathSegments].map(normalizeArtifactPathSegment).join('/'); export const buildAtlasDlqObjectPath = (namespace, ...pathSegments) => buildAtlasArtifactObjectPath(namespace, ...pathSegments); export const resolveAtlasNamespacedArtifactUri = (projectId, namespace, ...pathSegments) => resolveAtlasArtifactUri(projectId, buildAtlasArtifactObjectPath(namespace, ...pathSegments)); export const resolveAtlasNamespacedDlqUri = (projectId, namespace, ...pathSegments) => resolveAtlasDlqUri(projectId, buildAtlasDlqObjectPath(namespace, ...pathSegments)); export const parseGsUri = gsUri => { if (!isString(gsUri) || !gsUri.startsWith('gs://')) { throw new Error(`Atlas artifact destination must be a gs:// URI. Received ${gsUri}.`); } const normalizedGsUri = gsUri.trim().replace(/\/+$/g, ''); const pathPart = normalizedGsUri.slice('gs://'.length); const slashIndex = pathPart.indexOf('/'); if (slashIndex <= 0 || slashIndex === pathPart.length - 1) { throw new Error('Atlas artifact destination must include both a bucket name and an object path.'); } return { bucketName: pathPart.slice(0, slashIndex), objectPath: pathPart.slice(slashIndex + 1), uri: normalizedGsUri }; }; export const ensureAtlasArtifactBucket = ({ location = DEFAULT_ATLAS_ARTIFACT_BUCKET_LOCATION, projectId }, options = {}) => { const runCommand = options.runCommand ?? nodeExecFileSync; const bucketName = options.bucketName ?? (options.destinationUri ? parseGsUri(options.destinationUri).bucketName : null) ?? resolveAtlasArtifactBucketName(projectId); try { runBucketCommand(['storage', 'buckets', 'describe', `gs://${bucketName}`, `--project=${projectId}`], runCommand); return { bucketName, location, status: 'existing' }; } catch (error) { if (options.dryRun && !isMissingBucketError(error)) { return { bucketName, location, status: 'unverified' }; } if (!isMissingBucketError(error)) { throw new Error(`Failed to inspect Atlas artifact bucket gs://${bucketName}. ${getBucketInspectionErrorText(error)}`); } } if (options.dryRun) { return { bucketName, location, status: 'would-create' }; } try { runBucketCommand(['storage', 'buckets', 'create', `gs://${bucketName}`, `--project=${projectId}`, `--location=${location}`, '--uniform-bucket-level-access'], runCommand); } catch (error) { throw new Error(`Failed to create Atlas artifact bucket gs://${bucketName}. ${getBucketInspectionErrorText(error)}`); } return { bucketName, location, status: 'created' }; };