UNPKG

@puls-atlas/cli

Version:

The Puls Atlas CLI tool for managing Atlas projects

218 lines 12.4 kB
import * as features from '../../utils/feature.js'; import { logger } from '../../utils/index.js'; import { normalizeOptionalString } from '../../utils/value.js'; import { resolveServiceRuntimeInputs } from './terraformWorkflow.js'; import { bootstrapServiceClusterCredentials, createServiceTerraformConnection, doesServiceClusterExist, readServiceClusterJsonResource, readServiceClusterSecret, resolveServiceClusterConnection, resolveServiceClusterTarget, shouldBootstrapManagedServiceCluster } from './cluster.js'; import { logServiceDeploySummary, readServicePlatformTerraformOutputs, readServiceTerraformOutputs, runServicePlatformTerraformWorkflow, runServiceTerraformWorkflow, syncServiceAccessSecret, writeServicePlatformTerraformArtifact, writeServiceTerraformArtifact } from './serviceInfrastructure.js'; import { getService } from './serviceRegistry.js'; import { getCurrentServiceConfig, persistServiceConfig, resolveServiceSelection } from './selection.js'; const resolveServiceDeployHints = (service, cwd = process.cwd(), dependencies = {}) => service.resolveDeployHints(cwd, dependencies) ?? {}; const resolveManagedPlatformClusterConfig = (service, context, serviceConfig, serviceDeployHints = {}) => service.resolveManagedPlatformClusterConfig(context, serviceConfig, serviceDeployHints); const resolveManagedPlatformDefaultClusterConfig = (service, context, serviceConfig, serviceDeployHints = {}) => resolveManagedPlatformClusterConfig(service, context, { ...serviceConfig, clusterLocation: null, clusterName: null }, serviceDeployHints); const createMissingConfiguredClusterMessage = (service, clusterTarget, managedPlatformClusterConfig) => { const clusterPropertyPath = `${service.configSectionKey}.clusterName and ${service.configSectionKey}.clusterLocation`; return `Atlas could not find the configured GKE cluster ${clusterTarget.clusterName} in ${clusterTarget.clusterLocation}. ` + `Update ${clusterPropertyPath}, ${managedPlatformClusterConfig ? 'pass --create-cluster to provision the managed Atlas platform, or remove the stale cluster settings.' : 'or remove the stale cluster settings.'}`; }; export const runServiceDeploy = async (serviceName, options = {}, dependencies = {}, cwd = process.cwd()) => { const loadFeatureContextImpl = dependencies.loadFeatureContext ?? features.loadFeatureContext; const loggerImpl = dependencies.logger ?? logger; const service = getService(serviceName); const exit = dependencies.exit ?? (code => process.exit(code)); let spinner; try { const context = await loadFeatureContextImpl('services', options, { cwd }); const serviceDeployHints = resolveServiceDeployHints(service, cwd, dependencies); const currentServiceConfig = getCurrentServiceConfig(context.config, service.id); let selectedServiceConfig = await resolveServiceSelection(service.id, { ...currentServiceConfig }, options, dependencies); const managedPlatformClusterConfig = resolveManagedPlatformClusterConfig(service, context, selectedServiceConfig, serviceDeployHints); let shouldCreateCluster = shouldBootstrapManagedServiceCluster(service.id, managedPlatformClusterConfig, selectedServiceConfig, serviceDeployHints, options); if (shouldCreateCluster) { selectedServiceConfig = { ...selectedServiceConfig, clusterLocation: managedPlatformClusterConfig.location, clusterName: managedPlatformClusterConfig.name }; } else { const configuredClusterTarget = resolveServiceClusterTarget(service, selectedServiceConfig, serviceDeployHints); const clusterExists = doesServiceClusterExist(context, configuredClusterTarget, { runCommand: dependencies.runCommand, runGcloudFileCommand: dependencies.runGcloudFileCommand }); if (!clusterExists) { const managedPlatformDefaultClusterConfig = resolveManagedPlatformDefaultClusterConfig(service, context, selectedServiceConfig, serviceDeployHints); const matchesManagedDefaultCluster = Boolean(managedPlatformClusterConfig) && Boolean(managedPlatformDefaultClusterConfig) && configuredClusterTarget.clusterName === managedPlatformDefaultClusterConfig.name && configuredClusterTarget.clusterLocation === managedPlatformDefaultClusterConfig.location; if (!matchesManagedDefaultCluster) { throw new Error(createMissingConfiguredClusterMessage(service, configuredClusterTarget, managedPlatformClusterConfig)); } shouldCreateCluster = true; selectedServiceConfig = { ...selectedServiceConfig, clusterLocation: managedPlatformClusterConfig.location, clusterName: managedPlatformClusterConfig.name }; } } spinner = loggerImpl.spinner(options.dryRun ? `Preparing Atlas ${service.displayName} service dry run...` : `Deploying Atlas ${service.displayName} service...`); const nextRootConfig = persistServiceConfig(context, service.id, selectedServiceConfig, dependencies); const nextContext = { ...context, config: { ...context.config, services: { ...(context.config.services ?? {}), [service.id]: selectedServiceConfig } } }; spinner.stop(); loggerImpl.info(options.dryRun ? 'Atlas service dry run is ready. Running Terraform planning steps.' : 'Atlas service inputs are ready. Running Terraform deployment steps.'); let clusterTarget; let platformOutputs = null; let platformTerraformArtifact = null; let platformTerraformWorkflow = null; if (shouldCreateCluster) { loggerImpl.info('Atlas did not find a reusable GKE cluster for this service. Provisioning the managed GKE platform first.'); platformTerraformArtifact = writeServicePlatformTerraformArtifact(nextContext, service.id, selectedServiceConfig, serviceDeployHints, dependencies, cwd); platformTerraformWorkflow = await runServicePlatformTerraformWorkflow(nextContext, service.id, selectedServiceConfig, serviceDeployHints, platformTerraformArtifact, { dryRun: options.dryRun === true }, { ensureServicePlatformTerraformRoot: dependencies.ensureServicePlatformTerraformRoot, logger: loggerImpl, platformTemplateDirectory: dependencies.platformTemplateDirectory, runTerraformCommand: dependencies.runTerraformCommand }, cwd); if (options.dryRun) { loggerImpl.warning('Atlas planned the managed GKE platform phase only. ' + `Run atlas deploy service ${service.id} again without --dry-run after reviewing the cluster plan.`); return { cacheless: true, platformTerraformArtifact, platformTerraformWorkflow, rootConfig: nextRootConfig, serviceConfig: selectedServiceConfig, serviceName: service.id, status: 'dry-run', terraformArtifact: null, terraformWorkflow: null }; } platformOutputs = await readServicePlatformTerraformOutputs(nextContext, service.id, selectedServiceConfig, serviceDeployHints, { ensureServicePlatformTerraformRoot: dependencies.ensureServicePlatformTerraformRoot, platformTemplateDirectory: dependencies.platformTemplateDirectory, runTerraformCommand: dependencies.runTerraformCommand }, cwd); clusterTarget = { clusterLocation: normalizeOptionalString(platformOutputs.cluster_location) ?? selectedServiceConfig.clusterLocation, clusterName: normalizeOptionalString(platformOutputs.cluster_name) ?? selectedServiceConfig.clusterName, source: 'managed-platform' }; selectedServiceConfig = { ...selectedServiceConfig, clusterLocation: clusterTarget.clusterLocation, clusterName: clusterTarget.clusterName }; persistServiceConfig(nextContext, service.id, selectedServiceConfig, dependencies); } else { clusterTarget = resolveServiceClusterTarget(service, selectedServiceConfig, serviceDeployHints); } const bootstrapConnection = createServiceTerraformConnection(nextContext, clusterTarget, cwd, { kubeconfigPath: dependencies.kubeconfigPath }); const clusterCredentials = bootstrapServiceClusterCredentials(nextContext, clusterTarget, { kubeconfigPath: bootstrapConnection.kubeconfigPath, existsSyncImpl: dependencies.existsSync, mkdirSyncImpl: dependencies.mkdirSync, runCommand: dependencies.runCommand, writeFileSyncImpl: dependencies.writeFileSync }); const clusterConnection = resolveServiceClusterConnection(nextContext, clusterTarget, { clusterCaCertificate: platformOutputs?.cluster_ca_certificate, clusterEndpoint: platformOutputs?.cluster_endpoint, runCommand: dependencies.runCommand }); const terraformConnection = createServiceTerraformConnection(nextContext, clusterTarget, cwd, { clusterCaCertificate: clusterConnection.clusterCaCertificate, clusterEndpoint: clusterConnection.clusterEndpoint, kubeconfigPath: dependencies.kubeconfigPath }); const runtimeInputs = await resolveServiceRuntimeInputs(nextContext, service.id, selectedServiceConfig, terraformConnection, { requestServiceClusterApi: dependencies.requestServiceClusterApi, runCommand: dependencies.runCommand, runGcloudFileCommand: dependencies.runGcloudFileCommand }); const terraformArtifact = writeServiceTerraformArtifact(nextContext, service.id, selectedServiceConfig, { ...dependencies, runtimeInputs, terraformConnection }, cwd); const terraformWorkflow = await runServiceTerraformWorkflow(nextContext, service.id, selectedServiceConfig, serviceDeployHints, terraformArtifact, { dryRun: options.dryRun === true }, { ensureServiceTerraformRoot: dependencies.ensureServiceTerraformRoot, logger: loggerImpl, requestServiceClusterApi: dependencies.requestServiceClusterApi, runCommand: dependencies.runCommand, runGcloudFileCommand: dependencies.runGcloudFileCommand, runTerraformCommand: dependencies.runTerraformCommand, terraformConnection, templateDirectory: dependencies.templateDirectory }, cwd); const secretSync = options.dryRun ? { adminEmail: selectedServiceConfig.adminEmail ?? null, ingressIpAddress: null, secretName: selectedServiceConfig.accessSecret, status: 'dry-run', url: null, warnings: [] } : await syncServiceAccessSecret(nextContext, service.id, selectedServiceConfig, await readServiceTerraformOutputs(nextContext, service.id, selectedServiceConfig, serviceDeployHints, { ensureServiceTerraformRoot: dependencies.ensureServiceTerraformRoot, runTerraformCommand: dependencies.runTerraformCommand, terraformConnection, templateDirectory: dependencies.templateDirectory }, cwd), { requestServiceClusterApi: dependencies.requestServiceClusterApi, readServiceClusterSecret: dependencies.readServiceClusterSecret ?? readServiceClusterSecret, terraformConnection, upsertSecret: dependencies.upsertSecret }); if (!options.dryRun) { await service.validateDeployment(nextContext, selectedServiceConfig, terraformConnection, { readServiceClusterJsonResource, requestServiceClusterApi: dependencies.requestServiceClusterApi, runCommand: dependencies.runCommand, runGcloudFileCommand: dependencies.runGcloudFileCommand }); } loggerImpl.info(options.dryRun ? `Atlas ${service.displayName} service dry run completed successfully.` : `Atlas ${service.displayName} service deployed successfully.`); logServiceDeploySummary(nextContext, service.id, selectedServiceConfig, clusterTarget, terraformWorkflow, terraformArtifact, secretSync, loggerImpl); return { cacheless: true, clusterCredentials, platformTerraformArtifact, platformTerraformWorkflow, rootConfig: nextRootConfig, secretSync, serviceConfig: selectedServiceConfig, serviceName: service.id, status: options.dryRun ? 'dry-run' : 'deployed', terraformArtifact, terraformWorkflow }; } catch (error) { if (spinner) { spinner.fail(`Failed to deploy Atlas service ${serviceName}.`); } loggerImpl.error(error.message, false); return exit(1); } }; export default { runServiceDeploy };