UNPKG

@puls-atlas/cli

Version:

The Puls Atlas CLI tool for managing Atlas projects

219 lines 11.6 kB
import fs from 'fs'; import os from 'os'; import path from 'path'; import { isPlainObject } from 'es-toolkit/predicate'; import { readJsonFile } from '../../utils/file.js'; import { normalizeOptionalString } from '../../utils/value.js'; import { getCommandErrorMessage, isGcloudResourceNotFoundError, parseGcloudJsonOutput, runGcloudFileCommand } from '../../utils/gcloud.js'; export const RUNTIME_SERVICE_CATALOG_SCHEMA_VERSION = 1; export const DEFAULT_RUNTIME_SERVICE_CATALOG_PROJECT = 'puls-atlas-core'; export const DEFAULT_RUNTIME_SERVICE_CATALOG_LOCATION = 'europe-west1'; export const DEFAULT_RUNTIME_SERVICE_CATALOG_REPOSITORY = 'atlas-runtime-catalog'; export const DEFAULT_RUNTIME_SERVICE_CATALOG_PACKAGE = 'atlas-standalone-service-catalog'; export const DEFAULT_RUNTIME_SERVICE_CATALOG_FILE_NAME = 'catalog.json'; const createRuntimeServiceCatalogReference = (options = {}) => ({ fileName: normalizeOptionalString(options.fileName) ?? DEFAULT_RUNTIME_SERVICE_CATALOG_FILE_NAME, location: normalizeOptionalString(options.location) ?? DEFAULT_RUNTIME_SERVICE_CATALOG_LOCATION, packageName: normalizeOptionalString(options.packageName) ?? DEFAULT_RUNTIME_SERVICE_CATALOG_PACKAGE, projectId: normalizeOptionalString(options.projectId) ?? DEFAULT_RUNTIME_SERVICE_CATALOG_PROJECT, repository: normalizeOptionalString(options.repository) ?? DEFAULT_RUNTIME_SERVICE_CATALOG_REPOSITORY }); const createRuntimeServiceCatalogPackageLabel = reference => `${reference.projectId}/${reference.location}/${reference.repository}/${reference.packageName}`; const createRuntimeServiceCatalogDescription = reference => `Atlas runtime service catalog package ${createRuntimeServiceCatalogPackageLabel(reference)}`; const throwRuntimeServiceCatalogError = message => { throw new Error(message); }; const assertPlainObject = (value, propertyPath) => { if (!isPlainObject(value)) { throwRuntimeServiceCatalogError(`Invalid Atlas runtime service catalog. Expected "${propertyPath}" to be an object.`); } }; const assertString = (value, propertyPath) => { if (!normalizeOptionalString(value)) { throwRuntimeServiceCatalogError(`Invalid Atlas runtime service catalog. Expected "${propertyPath}" to be a non-empty string.`); } }; const createGcloudFailureError = (message, error) => { const details = normalizeOptionalString(getCommandErrorMessage(error)); return new Error(details ? `${message} ${details}` : message); }; const normalizeRuntimeServiceCatalogVersionLimit = value => { if (value === undefined || value === null || value === '') { return null; } const parsedValue = Number(value); if (!Number.isInteger(parsedValue) || parsedValue < 1) { throw new Error('Atlas runtime service catalog version limit must be a positive integer.'); } return parsedValue; }; export const resolveRuntimeServiceCatalogVersionId = versionEntry => { const normalizedStringEntry = normalizeOptionalString(versionEntry); if (normalizedStringEntry) { return normalizedStringEntry.includes('/versions/') ? normalizedStringEntry.split('/').at(-1) : normalizedStringEntry; } if (!isPlainObject(versionEntry)) { return null; } const versionName = normalizeOptionalString(versionEntry.name) ?? normalizeOptionalString(versionEntry.version); if (!versionName) { return null; } return versionName.includes('/versions/') ? versionName.split('/').at(-1) : versionName; }; export const resolveRuntimeServiceCatalogVersions = (options = {}) => { const reference = createRuntimeServiceCatalogReference(options); const description = createRuntimeServiceCatalogDescription(reference); const limit = normalizeRuntimeServiceCatalogVersionLimit(options.limit); let versionOutput = ''; try { versionOutput = runGcloudFileCommand(['artifacts', 'versions', 'list', `--package=${reference.packageName}`, `--project=${reference.projectId}`, `--location=${reference.location}`, `--repository=${reference.repository}`, '--sort-by=~update_time', ...(limit === null ? [] : [`--limit=${limit}`]), '--format=json'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }, options.runCommand); } catch (error) { if (isGcloudResourceNotFoundError(error)) { throw createGcloudFailureError(`${description} was not found in Artifact Registry.`, error); } throw createGcloudFailureError(`Could not list versions for ${description}.`, error); } const versions = parseGcloudJsonOutput(versionOutput, `Artifact Registry versions for ${description}`); if (!Array.isArray(versions)) { throw new Error(`Could not parse Artifact Registry versions for ${description}. Expected a JSON array from gcloud.`); } return [...new Set(versions.map(resolveRuntimeServiceCatalogVersionId).filter(Boolean))]; }; export const validateRuntimeServiceCatalog = catalog => { assertPlainObject(catalog, 'catalog'); if (catalog.version !== RUNTIME_SERVICE_CATALOG_SCHEMA_VERSION) { throwRuntimeServiceCatalogError('Invalid Atlas runtime service catalog. ' + `Unsupported schema version ${catalog.version}. ` + `Expected ${RUNTIME_SERVICE_CATALOG_SCHEMA_VERSION}.`); } assertString(catalog.generatedAt, 'generatedAt'); assertPlainObject(catalog.source, 'source'); assertString(catalog.source.repository, 'source.repository'); assertString(catalog.source.revision, 'source.revision'); assertPlainObject(catalog.services, 'services'); for (const [serviceName, service] of Object.entries(catalog.services)) { assertPlainObject(service, `services.${serviceName}`); assertString(service.id, `services.${serviceName}.id`); if (service.id !== serviceName) { throwRuntimeServiceCatalogError(`Invalid Atlas runtime service catalog. Expected services.${serviceName}.id to equal "${serviceName}".`); } assertString(service.displayName, `services.${serviceName}.displayName`); assertPlainObject(service.serviceDeploy, `services.${serviceName}.serviceDeploy`); if (service.serviceDeploy.enabled !== true) { throwRuntimeServiceCatalogError(`Invalid Atlas runtime service catalog. Expected services.${serviceName}.serviceDeploy.enabled to be true.`); } if (service.serviceDeploy.target !== 'cloud-run-service') { throwRuntimeServiceCatalogError('Invalid Atlas runtime service catalog. ' + `Expected services.${serviceName}.serviceDeploy.target to be "cloud-run-service".`); } assertPlainObject(service.runtime, `services.${serviceName}.runtime`); assertString(service.runtime.role, `services.${serviceName}.runtime.role`); assertString(service.runtime.target, `services.${serviceName}.runtime.target`); assertPlainObject(service.image, `services.${serviceName}.image`); assertString(service.image.registryProject, `services.${serviceName}.image.registryProject`); assertString(service.image.registryLocation, `services.${serviceName}.image.registryLocation`); assertString(service.image.repository, `services.${serviceName}.image.repository`); assertString(service.image.imageName, `services.${serviceName}.image.imageName`); assertString(service.image.tag, `services.${serviceName}.image.tag`); assertPlainObject(service.deploy, `services.${serviceName}.deploy`); } return catalog; }; export const resolveLatestRuntimeServiceCatalogVersion = (options = {}) => { const reference = createRuntimeServiceCatalogReference(options); const description = createRuntimeServiceCatalogDescription(reference); const latestVersion = resolveRuntimeServiceCatalogVersions({ ...options, limit: 1 })[0]; if (!latestVersion) { throw new Error(`Could not resolve the latest version for ${description}.`); } return latestVersion; }; export const downloadRuntimeServiceCatalog = (catalogVersion, options = {}) => { const normalizedCatalogVersion = normalizeOptionalString(catalogVersion); if (!normalizedCatalogVersion) { throw new Error('Atlas runtime service catalog download requires a catalog version.'); } const reference = createRuntimeServiceCatalogReference(options); const description = createRuntimeServiceCatalogDescription(reference); const readJsonFileImpl = options.readJsonFile ?? readJsonFile; const fsImpl = options.fsImpl ?? fs; const mkdtempSyncImpl = options.mkdtempSyncImpl ?? fs.mkdtempSync; const rmSyncImpl = options.rmSyncImpl ?? fs.rmSync; const tmpdirImpl = options.tmpdirImpl ?? os.tmpdir; const temporaryDirectory = mkdtempSyncImpl(path.join(tmpdirImpl(), 'atlas-runtime-service-catalog-')); const catalogPath = path.join(temporaryDirectory, reference.fileName); try { try { runGcloudFileCommand(['artifacts', 'generic', 'download', `--destination=${temporaryDirectory}`, `--name=${reference.fileName}`, `--package=${reference.packageName}`, `--project=${reference.projectId}`, `--location=${reference.location}`, `--repository=${reference.repository}`, `--version=${normalizedCatalogVersion}`], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }, options.runCommand); } catch (error) { if (isGcloudResourceNotFoundError(error)) { throw createGcloudFailureError(`${description} version ${normalizedCatalogVersion} was not found in Artifact Registry.`, error); } throw createGcloudFailureError(`Could not download ${description} version ${normalizedCatalogVersion}.`, error); } const catalog = readJsonFileImpl(catalogPath, {}, { fsImpl }); return { catalog: validateRuntimeServiceCatalog(catalog), catalogVersion: normalizedCatalogVersion, reference }; } finally { rmSyncImpl(temporaryDirectory, { force: true, recursive: true }); } }; export const resolveRuntimeServiceCatalog = (options = {}) => { const catalogVersion = normalizeOptionalString(options.catalogVersion) ?? resolveLatestRuntimeServiceCatalogVersion(options); return downloadRuntimeServiceCatalog(catalogVersion, options); }; export const getRuntimeServiceCatalogService = (catalog, serviceName, options = {}) => { validateRuntimeServiceCatalog(catalog); const normalizedServiceName = normalizeOptionalString(serviceName); if (!normalizedServiceName) { throw new Error('Atlas runtime service resolution requires a service name.'); } const exactService = catalog.services[normalizedServiceName]; if (exactService) { return exactService; } const lowerCaseServiceName = normalizedServiceName.toLowerCase(); const matchingServices = Object.values(catalog.services).filter(service => { const serviceId = normalizeOptionalString(service.id); return serviceId?.toLowerCase() === lowerCaseServiceName; }); if (matchingServices.length === 1) { return matchingServices[0]; } const versionLabel = normalizeOptionalString(options.catalogVersion) ? ` in catalog version ${options.catalogVersion}` : ''; throw new Error(`Atlas runtime service ${normalizedServiceName} was not found${versionLabel}.`); }; export const resolveRuntimeServiceCatalogService = (serviceName, options = {}) => { const resolvedCatalog = resolveRuntimeServiceCatalog(options); return { ...resolvedCatalog, service: getRuntimeServiceCatalogService(resolvedCatalog.catalog, serviceName, { catalogVersion: resolvedCatalog.catalogVersion }) }; }; export default { downloadRuntimeServiceCatalog, getRuntimeServiceCatalogService, resolveLatestRuntimeServiceCatalogVersion, resolveRuntimeServiceCatalog, resolveRuntimeServiceCatalogService, resolveRuntimeServiceCatalogVersions, resolveRuntimeServiceCatalogVersionId, validateRuntimeServiceCatalog };