UNPKG

@puls-atlas/cli

Version:

The Puls Atlas CLI tool for managing Atlas projects

249 lines 11.3 kB
import * as features from '../../utils/feature.js'; import { loadAtlasFeatureCache, logger, writeAtlasFeatureCache } from '../../utils/index.js'; import { resolveSyncConfigLocation, compileSyncPlan } from './config/syncConfig.js'; import { buildSyncMapperArtifact, executeSyncMapperUpload } from './mapperArtifacts.js'; import { uploadSyncRuntimeConfig } from './runtimeArtifacts.js'; import { writeSyncTerraformArtifact } from './terraformAdapter.js'; import { resolveSyncCloudRunDeployConfig } from './deploymentConfig.js'; import { assertNoUnsupportedEnabledSources, buildSyncRuntimeConfig, generateSyncRuntimeConfig } from './apply.js'; import { getSyncTerraformPrerequisiteTargets, runSyncTerraformWorkflow } from './terraformWorkflow.js'; import { createSyncReadAliasEntryRows, createSyncReadAliasSummaryRows, createSyncSchemaPlanEntryRows, createSyncSchemaPlanSummaryRows, inspectSyncReadAliases, reconcileSyncManagedSchemas, reconcileSyncReadAliases } from './schemaPlan.js'; const createSyncDeploySummaryRows = (context, deployResult) => [{ label: 'Project', value: context.projectId }, context.environment ? { label: 'Environment', value: context.environment } : null, { label: 'Region', value: deployResult.deployConfig?.region ?? 'unknown' }, { label: 'Service image', value: deployResult.deployConfig?.serviceImage ?? 'unknown' }, { label: 'Backfill job image', value: deployResult.deployConfig?.backfillJobImage ?? 'unknown' }, { label: 'Runtime config URI', value: deployResult.deployConfig?.runtimeConfigUri ?? 'unknown' }, { label: 'Mapper manifest URI', value: deployResult.deployConfig?.mapperManifestUri ?? 'unknown' }, { label: 'Terraform mode', value: deployResult.terraformResult?.mode ?? 'unknown' }, { label: 'Enabled pipelines', value: deployResult.generationResult?.enabledPipelineCount ?? 0 }, ...createSyncReadAliasSummaryRows(deployResult.generationResult?.runtimeConfig?.readAliasPlan), ...createSyncSchemaPlanSummaryRows(deployResult.generationResult?.runtimeConfig?.schemaPlan)]; export const runSyncDeploy = async (options = {}, dependencies = {}, workingDirectory = process.cwd()) => { const { buildSyncMapperArtifactImpl = buildSyncMapperArtifact, executeSyncMapperUploadImpl = executeSyncMapperUpload, generateSyncRuntimeConfigImpl = generateSyncRuntimeConfig, inspectSyncReadAliasesImpl = inspectSyncReadAliases, loadFeatureContextImpl = features.loadFeatureContext, loadFeatureCacheImpl = loadAtlasFeatureCache, loggerImpl = logger, reconcileSyncManagedSchemasImpl = reconcileSyncManagedSchemas, reconcileSyncReadAliasesImpl = reconcileSyncReadAliases, resolveProjectSelectionImpl = features.resolveProjectSelection, resolveSyncConfigLocationImpl = resolveSyncConfigLocation, runSyncTerraformWorkflowImpl = runSyncTerraformWorkflow, uploadRuntimeConfigImpl = uploadSyncRuntimeConfig, writeSyncTerraformArtifactImpl = writeSyncTerraformArtifact, writeFeatureCacheImpl = writeAtlasFeatureCache } = dependencies; let spinner; try { const projectSelection = await resolveProjectSelectionImpl(options); const configLocation = resolveSyncConfigLocationImpl(workingDirectory); if (!configLocation.hasPreferred) { loggerImpl.info(`Atlas sync config is not configured for project ${projectSelection.projectId}. Run "atlas sync init" first.`); return { projectId: projectSelection.projectId, reason: 'missing-config', status: 'skipped' }; } const context = await loadFeatureContextImpl('sync', options, { cwd: workingDirectory, resolveProjectSelectionImpl: async () => projectSelection }); assertNoUnsupportedEnabledSources(compileSyncPlan(context.config)); const deployConfig = resolveSyncCloudRunDeployConfig(context); const cacheArtifact = loadFeatureCacheImpl('sync', context.projectId, workingDirectory); const previewRuntimeConfig = buildSyncRuntimeConfig(context, { cacheArtifact }); const previewReadAliasPlan = await inspectSyncReadAliasesImpl(context, previewRuntimeConfig, { strict: false }); spinner = loggerImpl.spinner('Planning Atlas sync managed BigQuery schemas...'); const schemaPlan = await reconcileSyncManagedSchemasImpl(context, previewRuntimeConfig, { dryRun: options.dryRun === true, strict: true }); if (schemaPlan.blockingIssues.length > 0) { spinner.fail('Atlas sync managed BigQuery schema planning found blocking issues.'); for (const issue of schemaPlan.blockingIssues) { loggerImpl.error(issue, false); } return { blockingIssues: schemaPlan.blockingIssues, schemaPlan, status: 'failed' }; } spinner.succeed(options.dryRun === true ? 'Atlas sync managed BigQuery schema planning is ready.' : 'Atlas sync managed BigQuery schemas are ready.'); spinner = loggerImpl.spinner('Generating Atlas sync runtime config...'); const generationResult = generateSyncRuntimeConfigImpl(context, { cacheArtifact, dryRun: options.dryRun === true, runtimeConfig: { ...previewRuntimeConfig, ...(schemaPlan ? { schemaPlan } : {}), ...(previewReadAliasPlan ? { readAliasPlan: previewReadAliasPlan } : {}) }, readAliasPlan: previewReadAliasPlan, schemaPlan }, workingDirectory); spinner.succeed('Atlas sync runtime config generated.'); spinner = loggerImpl.spinner('Bundling Atlas sync mapper artifacts...'); const mapperArtifact = options.dryRun === true ? null : await buildSyncMapperArtifactImpl(context, {}, workingDirectory); if (mapperArtifact) { spinner.succeed(`Bundled ${Object.keys(mapperArtifact.manifest.mappers).length} mapper(s).`); } else { spinner.succeed('Skipped mapper bundling (dry run).'); } spinner = loggerImpl.spinner('Uploading Atlas sync mapper artifacts...'); if (options.dryRun !== true) { executeSyncMapperUploadImpl(mapperArtifact); spinner.succeed('Atlas sync mapper artifacts uploaded.'); } else { spinner.succeed('Skipped mapper upload (dry run).'); } spinner = loggerImpl.spinner('Uploading Atlas sync runtime config...'); uploadRuntimeConfigImpl(generationResult.runtimeConfigArtifact.filePath, deployConfig.runtimeConfigUri, { dryRun: options.dryRun === true }); spinner.succeed('Atlas sync runtime config uploaded.'); spinner = loggerImpl.spinner('Reconciling Atlas sync BigQuery read aliases...'); const readAliasPlan = await reconcileSyncReadAliasesImpl(context, generationResult.runtimeConfig, { dryRun: options.dryRun === true, strict: true }); if (readAliasPlan.blockingIssues.length > 0) { spinner.fail('Atlas sync BigQuery read alias planning found blocking issues.'); for (const issue of readAliasPlan.blockingIssues) { loggerImpl.error(issue, false); } return { blockingIssues: readAliasPlan.blockingIssues, readAliasPlan, status: 'failed' }; } generationResult.runtimeConfig = { ...generationResult.runtimeConfig, readAliasPlan }; spinner.succeed(options.dryRun === true ? 'Atlas sync BigQuery read alias planning is ready.' : 'Atlas sync BigQuery read aliases are ready.'); spinner = loggerImpl.spinner('Writing Atlas sync Terraform artifact...'); const terraformArtifact = writeSyncTerraformArtifactImpl(context, deployConfig.runtimeConfigUri, deployConfig.mapperManifestUri, { dryRun: options.dryRun === true }, workingDirectory); spinner.succeed('Atlas sync Terraform artifact ready.'); spinner = loggerImpl.spinner('Running Atlas sync Terraform workflow...'); if (options.dryRun !== true) { const prerequisiteTargets = getSyncTerraformPrerequisiteTargets(terraformArtifact, null); if (prerequisiteTargets.length > 0) { await runSyncTerraformWorkflowImpl(context.config, terraformArtifact, { targets: prerequisiteTargets }, dependencies, workingDirectory); } } const terraformResult = await runSyncTerraformWorkflowImpl(context.config, terraformArtifact, { dryRun: options.dryRun === true }, dependencies, workingDirectory); spinner.succeed(options.dryRun ? 'Atlas sync Terraform plan complete (dry run).' : 'Atlas sync Terraform workflow complete.'); const writtenCacheArtifact = options.dryRun === true ? null : writeFeatureCacheImpl('sync', context.projectId, { ...(cacheArtifact?.cache ?? {}), cachedAt: new Date().toISOString(), config: context.config, configFingerprint: generationResult.rollout?.configFingerprint ?? null, configPath: context.configPath, deployConfig, environment: context.environment, feature: context.featureName, mapperManifestUri: deployConfig.mapperManifestUri, projectId: context.projectId, release: generationResult.release, releaseTarget: generationResult.releaseTarget, readAliasPlan, rollout: generationResult.rollout, schemaPlan, runtimeConfigPath: generationResult.runtimeConfigArtifact.filePath, runtimeConfigUri: deployConfig.runtimeConfigUri, version: 1 }, workingDirectory); const deployResult = { deployConfig, generationResult, mapperArtifact, terraformResult }; loggerImpl.summary('Sync deploy summary', createSyncDeploySummaryRows(context, deployResult)); if (schemaPlan.entries.length > 0) { loggerImpl.summary('Managed schema targets', createSyncSchemaPlanEntryRows(schemaPlan)); } if (readAliasPlan.entries.length > 0) { loggerImpl.summary('BigQuery read aliases', createSyncReadAliasEntryRows(readAliasPlan)); } for (const warning of schemaPlan.warnings ?? []) { loggerImpl.warning(warning); } for (const warning of readAliasPlan.warnings ?? []) { loggerImpl.warning(warning); } loggerImpl.summary('Uploaded artifacts', [{ label: 'Runtime config', value: deployConfig.runtimeConfigUri }, { label: 'Mapper manifest', value: deployConfig.mapperManifestUri }]); if (writtenCacheArtifact) { loggerImpl.summary('Local files', [{ label: 'Runtime config', value: generationResult.runtimeConfigArtifact.filePath }, { label: 'Terraform artifact', value: terraformArtifact.filePath }, { label: 'Cache', value: writtenCacheArtifact.filePath }]); } if (options.dryRun === true) { loggerImpl.info('Dry run requested: Atlas did not upload artifacts or apply Terraform.'); } if (generationResult.enabledPipelineCount === 0) { loggerImpl.warning('Atlas sync config is valid, but no workloads are enabled for the selected project.'); } return { ...deployResult, cacheArtifact: writtenCacheArtifact, status: options.dryRun ? 'dry-run' : 'deployed' }; } catch (error) { spinner?.fail('Atlas sync deploy failed.'); loggerImpl.error(error.message); return null; } }; export default async options => runSyncDeploy(options);