UNPKG

@puls-atlas/cli

Version:

The Puls Atlas CLI tool for managing Atlas projects

382 lines 19.2 kB
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);