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