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