@puls-atlas/cli
Version:
The Puls Atlas CLI tool for managing Atlas projects
382 lines • 19.2 kB
JavaScript
import { isDeepStrictEqual } from 'node:util';
import { isPlainObject } from 'es-toolkit/predicate';
import * as features from '../../utils/feature.js';
import { readJsonFile } from '../../utils/file.js';
import { findService } from './serviceRegistry.js';
import { normalizeOptionalString } from '../../utils/value.js';
import { isGcloudResourceNotFoundError, logger, parseGcloudJsonOutput, runGcloudFileCommand } from '../../utils/index.js';
import { parseArtifactRegistryDockerImageReference, resolveArtifactRegistryDockerImageDigest } from '../search/runtimeRegistry.js';
import { SERVICE_RUNTIME_CONFIG_VERSION, createGeneratedServiceConfig, previewGeneratedServiceConfigArtifact } from './runtimeConfig.js';
import { getRuntimeServiceCatalogService, resolveRuntimeServiceCatalog } from './runtimeServiceCatalog.js';
const DEFAULT_RUNTIME_SERVICE_VERIFY_REGION = 'europe-west1';
const isObject = value => isPlainObject(value);
const createGeneratedConfigComparison = actualGeneratedConfig => ({
environment: actualGeneratedConfig?.environment ?? null,
projectId: actualGeneratedConfig?.projectId ?? null,
services: Object.fromEntries(Object.entries(isObject(actualGeneratedConfig?.services) ? actualGeneratedConfig.services : {}).map(([serviceName, serviceEntry]) => [serviceName, {
config: isObject(serviceEntry?.config) ? serviceEntry.config : {},
scope: serviceEntry?.scope ?? null
}])),
version: actualGeneratedConfig?.version ?? null
});
const resolveGeneratedRuntimeServiceUrl = serviceEntry => normalizeOptionalString(serviceEntry?.runtime?.serviceUrl);
const normalizeGeneratedServiceConfig = (generatedConfig, context, generatedConfigPath) => {
const issues = [];
if (generatedConfig === null) {
return {
config: null,
issues
};
}
if (!isObject(generatedConfig)) {
return {
config: {
environment: null,
projectId: context.projectId,
services: {},
version: null
},
issues: [`Generated desired-state file ${generatedConfigPath} does not contain a JSON object.`]
};
}
if (generatedConfig.version !== SERVICE_RUNTIME_CONFIG_VERSION) {
issues.push(`Generated desired-state file ${generatedConfigPath} uses version ${generatedConfig.version}, expected ${SERVICE_RUNTIME_CONFIG_VERSION}.`);
}
if (normalizeOptionalString(generatedConfig.projectId) !== context.projectId) {
issues.push(`Generated desired-state file ${generatedConfigPath} targets project ${generatedConfig.projectId ?? '(missing)'} instead of ${context.projectId}.`);
}
if (generatedConfig.environment !== undefined && generatedConfig.environment !== null && !normalizeOptionalString(generatedConfig.environment)) {
issues.push(`Generated desired-state file ${generatedConfigPath} has an invalid environment value.`);
}
if (!isObject(generatedConfig.services)) {
issues.push(`Generated desired-state file ${generatedConfigPath} is missing a services object.`);
}
return {
config: {
environment: generatedConfig.environment ?? null,
projectId: generatedConfig.projectId ?? null,
services: isObject(generatedConfig.services) ? generatedConfig.services : {},
version: generatedConfig.version ?? null
},
issues
};
};
const resolveRuntimeServiceCatalogCache = dependencies => {
const resolveRuntimeServiceCatalogImpl = dependencies.resolveRuntimeServiceCatalog ?? resolveRuntimeServiceCatalog;
const cache = new Map();
return catalogVersion => {
const normalizedCatalogVersion = normalizeOptionalString(catalogVersion) ?? '__latest__';
if (!cache.has(normalizedCatalogVersion)) {
cache.set(normalizedCatalogVersion, resolveRuntimeServiceCatalogImpl({
...(normalizedCatalogVersion === '__latest__' ? {} : {
catalogVersion: normalizedCatalogVersion
}),
runCommand: dependencies.runCommand
}));
}
return cache.get(normalizedCatalogVersion);
};
};
const createRemoteInspectionResult = (status, overrides = {}) => ({
image: null,
remoteDigest: null,
resourceName: null,
status,
url: null,
...overrides
});
const resolveRuntimeServiceRegion = (catalogService, expectedEntry) => normalizeOptionalString(expectedEntry?.config?.deploy?.region) ?? normalizeOptionalString(catalogService?.image?.registryLocation) ?? DEFAULT_RUNTIME_SERVICE_VERIFY_REGION;
const inspectRuntimeServiceCloudRunService = (context, catalogService, expectedEntry, dependencies = {}) => {
const runGcloudFileCommandImpl = dependencies.runGcloudFileCommand ?? runGcloudFileCommand;
const parseGcloudJsonOutputImpl = dependencies.parseGcloudJsonOutput ?? parseGcloudJsonOutput;
const resolveArtifactRegistryDockerImageDigestImpl = dependencies.resolveArtifactRegistryDockerImageDigest ?? resolveArtifactRegistryDockerImageDigest;
const region = resolveRuntimeServiceRegion(catalogService, expectedEntry);
const resourceName = catalogService.image.imageName;
try {
const output = runGcloudFileCommandImpl(['run', 'services', 'describe', resourceName, `--project=${context.projectId}`, `--region=${region}`, '--format=json'], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe']
}, dependencies.runCommand);
const description = parseGcloudJsonOutputImpl(output, `Cloud Run service ${resourceName}`);
const imageReference = normalizeOptionalString(description?.spec?.template?.spec?.containers?.[0]?.image) ?? null;
return createRemoteInspectionResult('deployed', {
image: imageReference,
remoteDigest: imageReference ? resolveArtifactRegistryDockerImageDigestImpl(imageReference, {
runCommand: dependencies.runCommand
}) : null,
resourceName,
url: normalizeOptionalString(description?.status?.url) ?? normalizeOptionalString(description?.status?.address?.url) ?? null
});
} catch (error) {
if (isGcloudResourceNotFoundError(error)) {
return createRemoteInspectionResult('not-deployed', {
resourceName
});
}
return createRemoteInspectionResult('unknown', {
detail: error.message,
resourceName
});
}
};
const resolveExpectedEntryEnabled = expectedEntry => expectedEntry?.config?.enabled === true;
const createServiceVerificationResult = serviceName => ({
catalogStatus: 'skipped',
desiredStateStatus: 'match',
enabled: false,
issues: [],
remoteStatus: 'skipped',
serviceName
});
const verifyRuntimeServiceAgainstCatalog = (serviceName, expectedEntry, dependencies, resolveCatalog) => {
const result = {
catalogService: null,
issues: [],
status: 'skipped'
};
if (!resolveExpectedEntryEnabled(expectedEntry)) {
return result;
}
const releaseConfig = isObject(expectedEntry?.config?.release) ? expectedEntry.config.release : {};
const catalogVersion = normalizeOptionalString(releaseConfig.catalogVersion);
const expectedImage = normalizeOptionalString(releaseConfig.image);
if (!catalogVersion) {
result.issues.push(`Enabled runtime service "${serviceName}" is missing services.${serviceName}.release.catalogVersion in the generated desired-state.`);
result.status = 'drift';
return result;
}
if (!expectedImage) {
result.issues.push(`Enabled runtime service "${serviceName}" is missing services.${serviceName}.release.image in the generated desired-state.`);
result.status = 'drift';
return result;
}
try {
const resolvedCatalog = resolveCatalog(catalogVersion);
const catalogService = getRuntimeServiceCatalogService(resolvedCatalog.catalog, serviceName, {
catalogVersion: resolvedCatalog.catalogVersion
});
const parsedExpectedImage = parseArtifactRegistryDockerImageReference(expectedImage);
if (!parsedExpectedImage) {
result.issues.push(`Enabled runtime service "${serviceName}" has an invalid pinned image reference ${expectedImage}.`);
result.status = 'drift';
return result;
}
if (catalogService.serviceDeploy.scope !== expectedEntry.scope) {
result.issues.push(`Runtime service "${serviceName}" uses scope ${expectedEntry.scope} in desired-state, but catalog version ${resolvedCatalog.catalogVersion} defines ${catalogService.serviceDeploy.scope}.`);
}
if (parsedExpectedImage.location !== catalogService.image.registryLocation || parsedExpectedImage.projectId !== catalogService.image.registryProject || parsedExpectedImage.repository !== catalogService.image.repository || parsedExpectedImage.imageName !== catalogService.image.imageName) {
result.issues.push(`Runtime service "${serviceName}" pins ${expectedImage}, which does not match catalog version ${resolvedCatalog.catalogVersion} for ${catalogService.image.imageName}.`);
}
result.catalogService = catalogService;
result.status = result.issues.length > 0 ? 'drift' : 'verified';
return result;
} catch (error) {
result.issues.push(error.message);
result.status = 'drift';
return result;
}
};
const createServiceSummaryRowValue = result => `desired-state=${result.desiredStateStatus}; catalog=${result.catalogStatus}; remote=${result.remoteStatus}`;
const createServiceSummaryRows = results => results.map(result => ({
label: result.serviceName,
tone: result.issues.length > 0 ? 'warning' : 'success',
value: createServiceSummaryRowValue(result)
}));
const createServiceDetailEntries = results => results.flatMap(result => result.issues.map(issue => `${result.serviceName}: ${issue}`));
const verifyServiceResults = async (context, options, workingDirectory, dependencies = {}) => {
const readJsonFileImpl = dependencies.readJsonFile ?? readJsonFile;
const resolveArtifactRegistryDockerImageDigestImpl = dependencies.resolveArtifactRegistryDockerImageDigest ?? resolveArtifactRegistryDockerImageDigest;
const generatedConfigPath = previewGeneratedServiceConfigArtifact(context, workingDirectory).filePath;
const expectedGeneratedConfig = createGeneratedServiceConfig(context, context.rootConfig ?? context.config ?? {
services: {}
});
const loadedGeneratedConfig = readJsonFileImpl(generatedConfigPath, {
allowMissing: true
});
const normalizedGeneratedConfig = normalizeGeneratedServiceConfig(loadedGeneratedConfig, context, generatedConfigPath);
const comparableExpectedConfig = createGeneratedConfigComparison(expectedGeneratedConfig);
const comparableActualConfig = createGeneratedConfigComparison(normalizedGeneratedConfig.config);
const resolveCatalog = resolveRuntimeServiceCatalogCache(dependencies);
const results = [];
const issues = [...normalizedGeneratedConfig.issues];
const serviceNames = Array.from(new Set([...Object.keys(comparableExpectedConfig.services ?? {}), ...Object.keys(comparableActualConfig.services ?? {})])).sort((left, right) => left.localeCompare(right));
if (normalizedGeneratedConfig.config === null && serviceNames.length > 0) {
issues.push(`Generated desired-state file ${generatedConfigPath} was not found. Generate or deploy the runtime service contract before verifying.`);
}
if (normalizedGeneratedConfig.config !== null && !isDeepStrictEqual({
environment: comparableActualConfig.environment,
projectId: comparableActualConfig.projectId,
version: comparableActualConfig.version
}, {
environment: comparableExpectedConfig.environment,
projectId: comparableExpectedConfig.projectId,
version: comparableExpectedConfig.version
})) {
issues.push(`Generated desired-state metadata in ${generatedConfigPath} does not match the current services context.`);
}
for (const serviceName of serviceNames) {
const result = createServiceVerificationResult(serviceName);
const expectedEntry = comparableExpectedConfig.services[serviceName] ?? null;
const actualEntry = comparableActualConfig.services[serviceName] ?? null;
const actualRuntimeEntry = normalizedGeneratedConfig.config?.services?.[serviceName] ?? null;
const builtInService = findService(serviceName);
result.enabled = resolveExpectedEntryEnabled(expectedEntry);
if (!expectedEntry && actualEntry) {
result.desiredStateStatus = 'unexpected';
result.issues.push(`Desired-state contains service "${serviceName}" in ${generatedConfigPath}, but it is not configured for project ${context.projectId}.`);
results.push(result);
continue;
}
if (expectedEntry && !actualEntry) {
result.desiredStateStatus = 'missing';
result.issues.push(`Desired-state is missing service "${serviceName}" in ${generatedConfigPath}.`);
} else if (expectedEntry && actualEntry && !isDeepStrictEqual(expectedEntry, actualEntry)) {
result.desiredStateStatus = 'drift';
result.issues.push(`Desired-state for service "${serviceName}" in ${generatedConfigPath} does not match services/config.json.`);
}
if (builtInService) {
result.catalogStatus = 'not-applicable';
result.remoteStatus = 'not-applicable';
results.push(result);
continue;
}
const catalogVerification = verifyRuntimeServiceAgainstCatalog(serviceName, expectedEntry, dependencies, resolveCatalog);
result.catalogStatus = catalogVerification.status;
result.issues.push(...catalogVerification.issues);
if (!expectedEntry || !resolveExpectedEntryEnabled(expectedEntry) || options.localOnly === true) {
result.remoteStatus = options.localOnly === true ? 'skipped' : 'skipped';
results.push(result);
continue;
}
if (!catalogVerification.catalogService) {
result.remoteStatus = 'skipped';
results.push(result);
continue;
}
const remoteInspection = inspectRuntimeServiceCloudRunService(context, catalogVerification.catalogService, expectedEntry, {
...dependencies,
resolveArtifactRegistryDockerImageDigest: resolveArtifactRegistryDockerImageDigestImpl
});
const expectedImage = normalizeOptionalString(expectedEntry?.config?.release?.image);
const generatedServiceUrl = resolveGeneratedRuntimeServiceUrl(actualRuntimeEntry);
if (remoteInspection.status === 'not-deployed') {
result.remoteStatus = 'not-deployed';
result.issues.push(`Cloud Run service ${remoteInspection.resourceName} was not found for runtime service "${serviceName}".`);
results.push(result);
continue;
}
if (remoteInspection.status === 'unknown') {
result.remoteStatus = 'unknown';
result.issues.push(`Cloud Run service ${remoteInspection.resourceName} for runtime service "${serviceName}" could not be verified. ${remoteInspection.detail}`);
results.push(result);
continue;
}
if (!generatedServiceUrl) {
result.remoteStatus = 'drift';
result.issues.push(`Generated desired-state for service "${serviceName}" in ${generatedConfigPath} is missing services.${serviceName}.runtime.serviceUrl.`);
results.push(result);
continue;
}
if (!remoteInspection.url) {
result.remoteStatus = 'drift';
result.issues.push(`Cloud Run service ${remoteInspection.resourceName} for runtime service "${serviceName}" does not expose status.url.`);
results.push(result);
continue;
}
if (generatedServiceUrl !== remoteInspection.url) {
result.remoteStatus = 'drift';
result.issues.push(`Generated desired-state for service "${serviceName}" in ${generatedConfigPath} points to ${generatedServiceUrl}, but Cloud Run reports ${remoteInspection.url}.`);
results.push(result);
continue;
}
const expectedDigest = expectedImage ? resolveArtifactRegistryDockerImageDigestImpl(expectedImage, {
runCommand: dependencies.runCommand
}) : null;
if (!expectedDigest || !remoteInspection.remoteDigest) {
result.remoteStatus = 'drift';
result.issues.push(`Cloud Run service ${remoteInspection.resourceName} for runtime service "${serviceName}" does not expose a comparable image digest.`);
results.push(result);
continue;
}
if (expectedDigest !== remoteInspection.remoteDigest) {
result.remoteStatus = 'drift';
result.issues.push(`Cloud Run service ${remoteInspection.resourceName} for runtime service "${serviceName}" runs ${remoteInspection.remoteDigest} instead of ${expectedDigest}.`);
results.push(result);
continue;
}
result.remoteStatus = 'verified';
results.push(result);
}
return {
generatedConfigPath,
issues: [...issues, ...createServiceDetailEntries(results)],
results
};
};
const createVerificationSummaryRows = (context, generatedConfigPath, results) => {
const enabledRuntimeServices = results.filter(result => result.enabled && result.catalogStatus !== 'not-applicable').length;
const driftedServices = results.filter(result => result.issues.length > 0).length;
return [{
label: 'Project',
value: context.projectId
}, context.environment ? {
label: 'Environment',
value: context.environment
} : null, {
label: 'Config',
value: context.configPath
}, {
label: 'Desired-state',
value: generatedConfigPath
}, {
label: 'Services',
value: results.length
}, {
label: 'Enabled runtime services',
value: enabledRuntimeServices
}, {
label: 'Drifted services',
value: driftedServices
}];
};
export const verifyServices = async (options, {
exitImpl = code => process.exit(code),
loadFeatureContextImpl = features.loadFeatureContext,
loggerImpl = logger,
workingDirectory = process.cwd(),
...dependencies
} = {}) => {
let spinner;
try {
const context = await loadFeatureContextImpl('services', options, {
cwd: workingDirectory
});
spinner = loggerImpl.spinner('Verifying Atlas services...');
const verification = await verifyServiceResults(context, options, workingDirectory, dependencies);
const summaryRows = createVerificationSummaryRows(context, verification.generatedConfigPath, verification.results);
if (verification.issues.length > 0) {
spinner.fail('Atlas service verification failed.');
loggerImpl.summary('Verification summary', summaryRows);
loggerImpl.summary('Service checks', createServiceSummaryRows(verification.results));
for (const issue of verification.issues) {
loggerImpl.error(issue, false);
}
exitImpl(1);
return;
}
spinner.succeed('Atlas service verification passed.');
loggerImpl.summary('Verification summary', summaryRows);
if (verification.results.length > 0) {
loggerImpl.summary('Service checks', createServiceSummaryRows(verification.results));
} else {
loggerImpl.warning('Atlas services config is valid, but no service contracts apply to the selected project.');
}
const verifiedRuntimeServices = verification.results.filter(result => result.enabled && result.catalogStatus === 'verified').length;
loggerImpl.info(`Atlas services verified ${verifiedRuntimeServices} enabled runtime service${verifiedRuntimeServices === 1 ? '' : 's'}.`);
} catch (error) {
spinner?.fail('Atlas service verification failed.');
loggerImpl.error(error.message, false);
exitImpl(1);
}
};
export default async options => verifyServices(options);