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