UNPKG

@puls-atlas/cli

Version:

The Puls Atlas CLI tool for managing Atlas projects

230 lines 11.3 kB
import { logger } from '../../utils/index.js'; import { getService } from './serviceRegistry.js'; import { normalizeOptionalString } from '../../utils/value.js'; import { createTerraformStringVariableArgument, runTerraformCommand } from '../../utils/terraform.js'; import { doesServiceClusterHelmReleaseExist, doesServiceClusterNamespaceExist, doesServiceClusterRoleBindingExist, doesServiceClusterRoleExist, doesServiceClusterServiceAccountExist, readServiceClusterJsonResource, readServiceClusterSecret, readServiceClusterTextResource } from './cluster.js'; const SERVICE_TERRAFORM_IMPORT_ALREADY_MANAGED_PATTERN = /already managed by Terraform/iu; const SERVICE_TERRAFORM_STATE_NOT_FOUND_PATTERN = /(no instance found for the given address|resource address .* does not exist in the state|no state file was found)/iu; const createServiceTerraformWorkflowCommand = (mode, terraformArtifact, inputVariableName) => { const baseArgs = [createTerraformStringVariableArgument(inputVariableName, terraformArtifact.filePath), '-input=false']; if (mode === 'plan') { return ['plan', ...baseArgs]; } return [mode, '-auto-approve', ...baseArgs]; }; const createServiceTerraformImportCommand = (terraformArtifact, target, inputVariableName) => ['import', createTerraformStringVariableArgument(inputVariableName, terraformArtifact.filePath), '-input=false', target.address, target.id]; const createServiceTerraformStateShowCommand = target => ['state', 'show', target.address]; const createServiceTerraformImportResult = (target, status) => ({ address: target.address, kind: target.kind, name: target.name, status }); const getServiceTerraformImportTargets = (context, serviceName, serviceConfig, terraformConnection, dependencies = {}) => { const service = getService(serviceName); if (typeof service.getTerraformImportTargets !== 'function') { return []; } return service.getTerraformImportTargets(context, serviceConfig, terraformConnection, { ...dependencies, doesServiceClusterHelmReleaseExist, doesServiceClusterNamespaceExist, doesServiceClusterRoleBindingExist, doesServiceClusterRoleExist, doesServiceClusterServiceAccountExist, readServiceClusterSecret }); }; const doesServiceTerraformImportTargetExist = async (target, dependencies = {}) => { const service = normalizeOptionalString(dependencies.serviceName) && typeof dependencies.serviceName === 'string' ? getService(dependencies.serviceName) : null; if (service && typeof service.doesTerraformImportTargetExist === 'function') { return service.doesTerraformImportTargetExist(target, dependencies); } if (typeof target.existsImpl === 'function') { return target.existsImpl(); } return false; }; export const importExistingServiceTerraformResources = async (context, serviceName, serviceConfig, terraformArtifact, workflowSummary, dependencies = {}) => { const importResults = []; const runTerraformCommandImpl = dependencies.runTerraformCommand ?? runTerraformCommand; const terraformEnvironment = dependencies.terraformConnection ? { KUBECONFIG: dependencies.terraformConnection.kubeconfigPath } : undefined; const inputVariableName = dependencies.terraformInputVariableName ?? 'atlas_service_tfvars_path'; for (const target of getServiceTerraformImportTargets(context, serviceName, serviceConfig, dependencies.terraformConnection, dependencies)) { if (!(await doesServiceTerraformImportTargetExist(target, { ...dependencies, serviceName }))) { importResults.push(createServiceTerraformImportResult(target, 'missing')); continue; } try { await runTerraformCommandImpl(createServiceTerraformStateShowCommand(target), { captureOutput: true, cwd: workflowSummary.rootPath, env: terraformEnvironment }); importResults.push(createServiceTerraformImportResult(target, 'already-managed')); continue; } catch (error) { if (!SERVICE_TERRAFORM_STATE_NOT_FOUND_PATTERN.test(error.message)) { throw error; } } try { await runTerraformCommandImpl(createServiceTerraformImportCommand(terraformArtifact, target, inputVariableName), { captureOutput: true, cwd: workflowSummary.rootPath, env: terraformEnvironment }); importResults.push(createServiceTerraformImportResult(target, 'imported')); } catch (error) { if (SERVICE_TERRAFORM_IMPORT_ALREADY_MANAGED_PATTERN.test(error.message)) { importResults.push(createServiceTerraformImportResult(target, 'already-managed')); continue; } throw error; } } return importResults; }; export const runServicePlatformTerraformWorkflow = async (context, serviceName, serviceConfig, searchRuntimeHints, terraformArtifact, options = {}, dependencies = {}, cwd = process.cwd()) => { const ensureTerraformRoot = dependencies.ensureServicePlatformTerraformRoot; if (typeof ensureTerraformRoot !== 'function') { throw new Error('Atlas service platform workflow requires ensureServicePlatformTerraformRoot.'); } const runTerraformCommandImpl = dependencies.runTerraformCommand ?? runTerraformCommand; const workflowSummary = await ensureTerraformRoot(context, serviceName, serviceConfig, searchRuntimeHints, cwd, dependencies); const mode = options.mode ?? (options.dryRun ? 'plan' : 'apply'); const initArgs = ['init', '-input=false']; const workflowArgs = createServiceTerraformWorkflowCommand(mode, terraformArtifact, dependencies.terraformInputVariableName ?? 'atlas_service_platform_tfvars_path'); const loggerImpl = dependencies.logger ?? logger; loggerImpl.info(`Starting terraform init in ${workflowSummary.rootPath}.`); await runTerraformCommandImpl(initArgs, { cwd: workflowSummary.rootPath, stdio: 'inherit' }); loggerImpl.info(`Starting terraform ${mode} in ${workflowSummary.rootPath}.`); await runTerraformCommandImpl(workflowArgs, { cwd: workflowSummary.rootPath, stdio: 'inherit' }); return { ...workflowSummary, commands: [initArgs, workflowArgs], importResults: [], mode }; }; export const readServicePlatformTerraformOutputs = async (context, serviceName, serviceConfig, searchRuntimeHints, dependencies = {}, cwd = process.cwd()) => { const ensureTerraformRoot = dependencies.ensureServicePlatformTerraformRoot; if (typeof ensureTerraformRoot !== 'function') { throw new Error('Atlas service platform outputs require ensureServicePlatformTerraformRoot.'); } const runTerraformCommandImpl = dependencies.runTerraformCommand ?? runTerraformCommand; const workflowSummary = await ensureTerraformRoot(context, serviceName, serviceConfig, searchRuntimeHints, cwd, dependencies); const outputText = await runTerraformCommandImpl(['output', '-json'], { captureOutput: true, cwd: workflowSummary.rootPath }); const parsedOutput = JSON.parse(outputText); return Object.fromEntries(Object.entries(parsedOutput).map(([key, value]) => [key, value?.value ?? null])); }; export const resolveServiceRuntimeInputs = async (context, serviceName, serviceConfig, terraformConnection, dependencies = {}) => { const service = getService(serviceName); if (typeof service.resolveRuntimeInputs !== 'function') { return {}; } return service.resolveRuntimeInputs(context, serviceConfig, terraformConnection, { ...dependencies, readServiceClusterSecret }); }; export const runServiceTerraformWorkflow = async (context, serviceName, serviceConfig, searchRuntimeHints, terraformArtifact, options = {}, dependencies = {}, cwd = process.cwd()) => { const ensureTerraformRoot = dependencies.ensureServiceTerraformRoot; if (typeof ensureTerraformRoot !== 'function') { throw new Error('Atlas service workflow requires ensureServiceTerraformRoot.'); } const service = getService(serviceName); const runTerraformCommandImpl = dependencies.runTerraformCommand ?? runTerraformCommand; const workflowSummary = await ensureTerraformRoot({ ...context.config, services: { ...(context.config.services ?? {}), [serviceName]: serviceConfig } }, serviceName, searchRuntimeHints, cwd, dependencies); const mode = options.mode ?? (options.dryRun ? 'plan' : 'apply'); const initArgs = ['init', '-input=false']; const workflowArgs = createServiceTerraformWorkflowCommand(mode, terraformArtifact, dependencies.terraformInputVariableName ?? 'atlas_service_tfvars_path'); const importResults = []; const loggerImpl = dependencies.logger ?? logger; loggerImpl.info(`Starting terraform init in ${workflowSummary.rootPath}.`); await runTerraformCommandImpl(initArgs, { cwd: workflowSummary.rootPath, env: dependencies.terraformConnection ? { KUBECONFIG: dependencies.terraformConnection.kubeconfigPath } : undefined, stdio: 'inherit' }); if (mode !== 'plan') { importResults.push(...(await importExistingServiceTerraformResources(context, serviceName, serviceConfig, terraformArtifact, workflowSummary, { ...dependencies, runTerraformCommand: runTerraformCommandImpl, serviceName }))); } loggerImpl.info(`Starting terraform ${mode} in ${workflowSummary.rootPath}.`); try { await runTerraformCommandImpl(workflowArgs, { cwd: workflowSummary.rootPath, env: dependencies.terraformConnection ? { KUBECONFIG: dependencies.terraformConnection.kubeconfigPath } : undefined, stdio: 'inherit' }); } catch (error) { if (mode !== 'plan' && typeof service.logTerraformApplyFailureDetails === 'function') { await service.logTerraformApplyFailureDetails(serviceConfig, loggerImpl, { readServiceClusterJsonResource, readServiceClusterTextResource, requestServiceClusterApi: dependencies.requestServiceClusterApi, runCommand: dependencies.runCommand, runGcloudFileCommand: dependencies.runGcloudFileCommand, terraformConnection: dependencies.terraformConnection }); } throw error; } return { ...workflowSummary, commands: [initArgs, workflowArgs], importResults, mode }; }; export const readServiceTerraformOutputs = async (context, serviceName, serviceConfig, searchRuntimeHints, dependencies = {}, cwd = process.cwd()) => { const ensureTerraformRoot = dependencies.ensureServiceTerraformRoot; if (typeof ensureTerraformRoot !== 'function') { throw new Error('Atlas service outputs require ensureServiceTerraformRoot.'); } const runTerraformCommandImpl = dependencies.runTerraformCommand ?? runTerraformCommand; const workflowSummary = await ensureTerraformRoot({ ...context.config, services: { ...(context.config.services ?? {}), [serviceName]: serviceConfig } }, serviceName, searchRuntimeHints, cwd, dependencies); const outputText = await runTerraformCommandImpl(['output', '-json'], { captureOutput: true, cwd: workflowSummary.rootPath, env: dependencies.terraformConnection ? { KUBECONFIG: dependencies.terraformConnection.kubeconfigPath } : undefined }); const parsedOutput = JSON.parse(outputText); return Object.fromEntries(Object.entries(parsedOutput).map(([key, value]) => [key, value?.value ?? null])); };