UNPKG

@puls-atlas/cli

Version:

The Puls Atlas CLI tool for managing Atlas projects

185 lines 7.97 kB
import * as features from '../../utils/feature.js'; import { isGcloudResourceNotFoundError, logger, runGcloudFileCommand } from '../../utils/index.js'; import { compileSyncPlan, DEFAULT_SYNC_TASK_QUEUE_NAME } from './config/syncConfig.js'; import { buildSyncRuntimeConfig } from './apply.js'; import { resolveSyncCloudRunDeployConfig } from './deploymentConfig.js'; import { SYNC_BACKFILL_JOB_NAME, SYNC_SERVICE_NAME } from './resourceNames.js'; import { createSyncReadAliasEntryRows, createSyncReadAliasSummaryRows, createSyncSchemaPlanEntryRows, createSyncSchemaPlanSummaryRows, inspectSyncManagedSchemas, inspectSyncReadAliases } from './schemaPlan.js'; const checkGcsObjectAccessible = gsUri => { try { runGcloudFileCommand(['storage', 'cat', '--range=0-0', gsUri], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }); return 'accessible'; } catch (error) { if (isGcloudResourceNotFoundError(error)) { return 'not-found'; } return 'inaccessible'; } }; const checkCloudRunServiceDeployed = (projectId, region, serviceName) => { try { runGcloudFileCommand(['run', 'services', 'describe', serviceName, `--project=${projectId}`, `--region=${region}`, '--format=value(name)'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }); return 'deployed'; } catch (error) { if (isGcloudResourceNotFoundError(error)) { return 'not-deployed'; } return 'unknown'; } }; const checkCloudRunJobDeployed = (projectId, region, jobName) => { try { runGcloudFileCommand(['run', 'jobs', 'describe', jobName, `--project=${projectId}`, `--region=${region}`, '--format=value(name)'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }); return 'deployed'; } catch (error) { if (isGcloudResourceNotFoundError(error)) { return 'not-deployed'; } return 'unknown'; } }; const checkCloudTasksQueueExists = (projectId, location, queueName) => { try { runGcloudFileCommand(['tasks', 'queues', 'describe', queueName, `--location=${location}`, `--project=${projectId}`, '--format=value(name)'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }); return 'deployed'; } catch (error) { if (isGcloudResourceNotFoundError(error)) { return 'not-deployed'; } return 'unknown'; } }; const createRemoteVerificationRows = remoteChecks => [{ label: `Cloud Run service (${SYNC_SERVICE_NAME})`, value: remoteChecks.service === 'deployed' ? 'OK' : `NOT OK (${remoteChecks.service})` }, { label: `Backfill job (${SYNC_BACKFILL_JOB_NAME})`, value: remoteChecks.backfillJob === 'deployed' ? 'OK' : `NOT OK (${remoteChecks.backfillJob})` }, { label: 'Runtime config URI', value: remoteChecks.runtimeConfigUri === 'accessible' ? 'OK' : `NOT OK (${remoteChecks.runtimeConfigUri})` }, { label: 'Mapper manifest URI', value: remoteChecks.mapperManifestUri === 'accessible' ? 'OK' : `NOT OK (${remoteChecks.mapperManifestUri})` }, { label: 'Task queue', value: remoteChecks.taskQueue === 'deployed' ? 'OK' : `NOT OK (${remoteChecks.taskQueue})` }]; const createSyncVerificationRows = (context, syncPlan) => [{ label: 'Project', value: context.projectId }, context.environment ? { label: 'Environment', value: context.environment } : null, { label: 'Version', value: context.config.version }, { label: 'Verified workloads', value: Object.keys(context.config?.workloads ?? {}).length }, { label: 'Enabled pipelines', value: syncPlan.pipelines.length }, { label: 'Sources', value: Object.keys(context.config?.sources ?? {}).length }, { label: 'Destinations', value: Object.keys(context.config?.destinations ?? {}).length }]; export const verifySyncConfig = async (options, { compileSyncPlanImpl = compileSyncPlan, inspectSyncManagedSchemasImpl = inspectSyncManagedSchemas, inspectSyncReadAliasesImpl = inspectSyncReadAliases, loadFeatureContextImpl = features.loadFeatureContext, loggerImpl = logger, workingDirectory = process.cwd() } = {}) => { let spinner; try { const context = await loadFeatureContextImpl('sync', options, { cwd: workingDirectory }); spinner = loggerImpl.spinner('Verifying Atlas sync config...'); const syncPlan = compileSyncPlanImpl(context.config); spinner.succeed('Atlas sync config verified.'); loggerImpl.summary('Verification summary', createSyncVerificationRows(context, syncPlan)); loggerImpl.summary('Local files', [{ label: 'Config', value: context.configPath }, { label: 'Workload files', value: context.configPaths.length }]); if (!options.localOnly) { const deployConfig = resolveSyncCloudRunDeployConfig(context); const remoteSpinner = loggerImpl.spinner('Checking remote Atlas sync resources...'); const previewRuntimeConfig = buildSyncRuntimeConfig({ ...context, syncPlan }); const taskQueueName = context.config?.deploy?.taskQueue?.queueName ?? DEFAULT_SYNC_TASK_QUEUE_NAME; const taskQueueLocation = context.config?.deploy?.taskQueue?.location ?? deployConfig.region; const remoteChecks = { service: checkCloudRunServiceDeployed(context.projectId, deployConfig.region, SYNC_SERVICE_NAME), backfillJob: checkCloudRunJobDeployed(context.projectId, deployConfig.region, SYNC_BACKFILL_JOB_NAME), runtimeConfigUri: deployConfig.runtimeConfigUri ? checkGcsObjectAccessible(deployConfig.runtimeConfigUri) : 'not-configured', mapperManifestUri: deployConfig.mapperManifestUri ? checkGcsObjectAccessible(deployConfig.mapperManifestUri) : 'not-configured', taskQueue: checkCloudTasksQueueExists(context.projectId, taskQueueLocation, taskQueueName) }; const schemaPlan = await inspectSyncManagedSchemasImpl(context, previewRuntimeConfig, { strict: true }); const readAliasPlan = await inspectSyncReadAliasesImpl(context, previewRuntimeConfig, { strict: true }); remoteSpinner.succeed('Remote Atlas sync resource verification complete.'); loggerImpl.summary('Remote resource checks', createRemoteVerificationRows(remoteChecks)); if (schemaPlan.entries.length > 0) { loggerImpl.summary('Managed schema targets', createSyncSchemaPlanEntryRows(schemaPlan)); loggerImpl.summary('Managed schema summary', createSyncSchemaPlanSummaryRows(schemaPlan)); } if (readAliasPlan.entries.length > 0) { loggerImpl.summary('BigQuery read aliases', createSyncReadAliasEntryRows(readAliasPlan)); loggerImpl.summary('BigQuery read alias summary', createSyncReadAliasSummaryRows(readAliasPlan)); } for (const warning of schemaPlan.warnings ?? []) { loggerImpl.warning(warning); } for (const warning of readAliasPlan.warnings ?? []) { loggerImpl.warning(warning); } const failedChecks = Object.values(remoteChecks).filter(status => status !== 'deployed' && status !== 'accessible'); if (failedChecks.length > 0) { throw new Error(`${failedChecks.length} remote resource check(s) failed. Run "atlas sync deploy" to provision missing resources.`); } if (schemaPlan.blockingIssues.length > 0) { throw new Error(schemaPlan.blockingIssues.join(' ')); } if (readAliasPlan.blockingIssues.length > 0) { throw new Error(readAliasPlan.blockingIssues.join(' ')); } } if (syncPlan.pipelines.length === 0) { loggerImpl.warning('Atlas sync config is valid, but no workloads are enabled for the selected project.'); return; } loggerImpl.info(`Atlas sync verified ${syncPlan.pipelines.length} enabled workload${syncPlan.pipelines.length === 1 ? '' : 's'}.`); } catch (error) { spinner?.fail('Failed to verify Atlas sync config.'); loggerImpl.error(error.message); } }; export default async options => verifySyncConfig(options);