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