@puls-atlas/cli
Version:
The Puls Atlas CLI tool for managing Atlas projects
380 lines • 19 kB
JavaScript
import fs from 'fs';
import path from 'path';
import { getAtlasGeneratedTerraformDir, resolveRootPath } from '../../utils/atlas.js';
import { writeJsonFile } from '../../utils/file.js';
import { logger } from '../../utils/index.js';
import { upsertSecret } from '../../utils/secrets.js';
import { normalizeOptionalString } from '../../utils/value.js';
import { getCurrentServiceConfig } from './selection.js';
import { getService } from './serviceRegistry.js';
import { createDefaultServicePlatformClusterConfig, createDefaultServicePlatformNodePoolConfig, createServiceResourceLabels } from './platform.js';
import { loadTerraformRootTemplateFiles, renderTerraformRootTemplateFiles, resolveSiblingTerraformModuleSource } from '../../utils/terraformRootTemplates.js';
import { importExistingServiceTerraformResources, readServicePlatformTerraformOutputs as readServicePlatformTerraformOutputsImpl, readServiceTerraformOutputs as readServiceTerraformOutputsImpl, runServicePlatformTerraformWorkflow as runServicePlatformTerraformWorkflowImpl, runServiceTerraformWorkflow as runServiceTerraformWorkflowImpl } from './terraformWorkflow.js';
export const SERVICE_TERRAFORM_CONTRACT_VERSION = 1;
export const SERVICE_TERRAFORM_INPUT_VARIABLE = 'atlas_service_tfvars_path';
export const SERVICE_PLATFORM_TERRAFORM_CONTRACT_VERSION = 1;
export const SERVICE_PLATFORM_TERRAFORM_INPUT_VARIABLE = 'atlas_service_platform_tfvars_path';
export const DEFAULT_SERVICE_PLATFORM_TERRAFORM_ROOT_DIR = 'services/terraform/platform';
const SERVICE_PLATFORM_TEMPLATE_ROOT_DIRECTORIES = ['templates/search-provider-platform-root'];
export { importExistingServiceTerraformResources };
export const resolveServiceTerraformConfig = (serviceConfig, serviceName, serviceDeployHints = {}) => {
const service = getService(serviceName);
const configuredModuleSource = normalizeOptionalString(serviceConfig.moduleSource);
let inferredModuleSource = null;
if (normalizeOptionalString(serviceDeployHints.providerModuleSource)) {
try {
inferredModuleSource = resolveSiblingTerraformModuleSource(serviceDeployHints.providerModuleSource, service.moduleRelativePath);
} catch {
inferredModuleSource = null;
}
}
return {
moduleSource: configuredModuleSource ?? inferredModuleSource ?? service.defaultModuleSource,
rootDir: normalizeOptionalString(serviceConfig.rootDir) ?? service.defaultRootDir,
templateRootDirectories: service.templateRootDirectories ?? []
};
};
export const resolveServicePlatformTerraformConfig = (serviceConfig, serviceName, serviceDeployHints = {}) => {
const serviceTerraformConfig = resolveServiceTerraformConfig(serviceConfig, serviceName, serviceDeployHints);
return {
clusterModuleSource: resolveSiblingTerraformModuleSource(serviceTerraformConfig.moduleSource, 'platform/gke-cluster'),
nodePoolModuleSource: resolveSiblingTerraformModuleSource(serviceTerraformConfig.moduleSource, 'platform/gke-node-pool'),
rootDir: DEFAULT_SERVICE_PLATFORM_TERRAFORM_ROOT_DIR,
templateRootDirectories: SERVICE_PLATFORM_TEMPLATE_ROOT_DIRECTORIES
};
};
export const resolveServiceTerraformRootPath = (serviceConfig, serviceName, serviceDeployHints = {}, cwd = process.cwd()) => resolveRootPath(resolveServiceTerraformConfig(serviceConfig, serviceName, serviceDeployHints).rootDir, cwd);
export const createServiceTerraformWorkflowSummary = (context, serviceName, serviceConfig, serviceDeployHints = {}, cwd = process.cwd()) => {
const service = getService(serviceName);
const terraformConfig = resolveServiceTerraformConfig(serviceConfig, serviceName, serviceDeployHints);
return {
managedResources: [{
description: service.description,
kind: serviceName,
name: serviceConfig.releaseName ?? serviceName
}],
moduleSource: terraformConfig.moduleSource,
rootDir: terraformConfig.rootDir,
rootPath: resolveRootPath(terraformConfig.rootDir, cwd),
templateRootDirectories: terraformConfig.templateRootDirectories
};
};
export const createServicePlatformTerraformWorkflowSummary = (context, serviceName, serviceConfig, serviceDeployHints = {}, cwd = process.cwd()) => {
const terraformConfig = resolveServicePlatformTerraformConfig(serviceConfig, serviceName, serviceDeployHints);
const clusterConfig = createDefaultServicePlatformClusterConfig(context, serviceConfig, serviceDeployHints);
const nodePoolConfig = createDefaultServicePlatformNodePoolConfig(serviceConfig);
return {
clusterModuleSource: terraformConfig.clusterModuleSource,
managedResources: [{
description: 'Managed GKE cluster foundation for Atlas services.',
kind: 'gke-cluster',
name: clusterConfig.name
}, {
description: 'Managed GKE node pool for Atlas service workloads.',
kind: 'gke-node-pool',
name: nodePoolConfig.name
}],
nodePoolModuleSource: terraformConfig.nodePoolModuleSource,
rootDir: terraformConfig.rootDir,
rootPath: resolveRootPath(terraformConfig.rootDir, cwd),
templateRootDirectories: terraformConfig.templateRootDirectories
};
};
const loadServiceTerraformRootTemplateFiles = async (moduleSource, rootPath, templateRootDirectories, options = {}) => {
const candidateDirectories = (templateRootDirectories ?? []).filter(Boolean);
const errors = [];
for (const templateRootDirectory of candidateDirectories) {
try {
return await loadTerraformRootTemplateFiles(moduleSource, rootPath, templateRootDirectory, options);
} catch (error) {
errors.push(error.message);
}
}
throw new Error(errors.at(-1) ?? `Could not load Terraform root templates for module ${moduleSource}.`);
};
const loadServicePlatformTerraformRootTemplateFiles = async (clusterModuleSource, rootPath, templateRootDirectories, options = {}) => {
const candidateDirectories = (templateRootDirectories ?? []).filter(Boolean);
const errors = [];
for (const templateRootDirectory of candidateDirectories) {
try {
return await loadTerraformRootTemplateFiles(clusterModuleSource, rootPath, templateRootDirectory, options);
} catch (error) {
errors.push(error.message);
}
}
throw new Error(errors.at(-1) ?? `Could not load Terraform root templates for module ${clusterModuleSource}.`);
};
export const ensureServiceTerraformRoot = async (serviceConfig, serviceName, serviceDeployHints = {}, cwd = process.cwd(), options = {}) => {
const {
existsSyncImpl = fs.existsSync,
mkdirSyncImpl = fs.mkdirSync,
readFileSyncImpl = fs.readFileSync,
templateDirectory,
writeFileSyncImpl = fs.writeFileSync
} = options;
const workflowSummary = createServiceTerraformWorkflowSummary({
config: serviceConfig
}, serviceName, getCurrentServiceConfig(serviceConfig, serviceName), serviceDeployHints, cwd);
const templateFiles = await loadServiceTerraformRootTemplateFiles(workflowSummary.moduleSource, cwd, workflowSummary.templateRootDirectories, {
...options,
templateDirectory
});
const rootFiles = renderTerraformRootTemplateFiles(templateFiles, {
__ATLAS_SEARCH_WORKLOAD_TERRAFORM_CONTRACT_VERSION__: SERVICE_TERRAFORM_CONTRACT_VERSION,
__ATLAS_SEARCH_WORKLOAD_TERRAFORM_INPUT_VARIABLE__: SERVICE_TERRAFORM_INPUT_VARIABLE,
__ATLAS_SEARCH_WORKLOAD_TERRAFORM_MODULE_SOURCE__: workflowSummary.moduleSource,
__ATLAS_SERVICE_TERRAFORM_CONTRACT_VERSION__: SERVICE_TERRAFORM_CONTRACT_VERSION,
__ATLAS_SERVICE_TERRAFORM_INPUT_VARIABLE__: SERVICE_TERRAFORM_INPUT_VARIABLE,
__ATLAS_SERVICE_TERRAFORM_MODULE_SOURCE__: workflowSummary.moduleSource
});
const createdFiles = [];
const updatedFiles = [];
if (!existsSyncImpl(workflowSummary.rootPath)) {
mkdirSyncImpl(workflowSummary.rootPath, {
recursive: true
});
}
for (const [fileName, content] of Object.entries(rootFiles)) {
const filePath = path.join(workflowSummary.rootPath, fileName);
if (!existsSyncImpl(filePath)) {
writeFileSyncImpl(filePath, content);
createdFiles.push(filePath);
continue;
}
if (readFileSyncImpl(filePath, 'utf-8') === content) {
continue;
}
writeFileSyncImpl(filePath, content);
updatedFiles.push(filePath);
}
return {
...workflowSummary,
createdFiles,
updatedFiles
};
};
export const ensureServicePlatformTerraformRoot = async (context, serviceName, serviceConfig, serviceDeployHints = {}, cwd = process.cwd(), options = {}) => {
const {
existsSyncImpl = fs.existsSync,
mkdirSyncImpl = fs.mkdirSync,
platformTemplateDirectory,
readFileSyncImpl = fs.readFileSync,
writeFileSyncImpl = fs.writeFileSync
} = options;
const workflowSummary = createServicePlatformTerraformWorkflowSummary(context, serviceName, serviceConfig, serviceDeployHints, cwd);
const templateFiles = await loadServicePlatformTerraformRootTemplateFiles(workflowSummary.clusterModuleSource, cwd, workflowSummary.templateRootDirectories, {
...options,
templateDirectory: platformTemplateDirectory
});
const rootFiles = renderTerraformRootTemplateFiles(templateFiles, {
__ATLAS_SEARCH_PROVIDER_GKE_CLUSTER_MODULE_SOURCE__: workflowSummary.clusterModuleSource,
__ATLAS_SEARCH_PROVIDER_GKE_NODE_POOL_MODULE_SOURCE__: workflowSummary.nodePoolModuleSource,
__ATLAS_SEARCH_PROVIDER_PLATFORM_TERRAFORM_CONTRACT_VERSION__: SERVICE_PLATFORM_TERRAFORM_CONTRACT_VERSION,
__ATLAS_SEARCH_PROVIDER_PLATFORM_TERRAFORM_INPUT_VARIABLE__: SERVICE_PLATFORM_TERRAFORM_INPUT_VARIABLE
});
const createdFiles = [];
const updatedFiles = [];
if (!existsSyncImpl(workflowSummary.rootPath)) {
mkdirSyncImpl(workflowSummary.rootPath, {
recursive: true
});
}
for (const [fileName, content] of Object.entries(rootFiles)) {
const filePath = path.join(workflowSummary.rootPath, fileName);
if (!existsSyncImpl(filePath)) {
writeFileSyncImpl(filePath, content);
createdFiles.push(filePath);
continue;
}
if (readFileSyncImpl(filePath, 'utf-8') === content) {
continue;
}
writeFileSyncImpl(filePath, content);
updatedFiles.push(filePath);
}
return {
...workflowSummary,
createdFiles,
updatedFiles
};
};
export const createServiceTerraformPayload = (context, serviceName, serviceConfig, options = {}) => {
const service = getService(serviceName);
return service.createTerraformPayload(context, serviceConfig, {
contractVersion: SERVICE_TERRAFORM_CONTRACT_VERSION,
resourceLabels: createServiceResourceLabels(context, serviceName),
runtimeInputs: options.runtimeInputs,
terraformConnection: options.terraformConnection
});
};
export const createServicePlatformTerraformPayload = (context, serviceName, serviceConfig, serviceDeployHints = {}) => {
const clusterConfig = createDefaultServicePlatformClusterConfig(context, serviceConfig, serviceDeployHints);
const nodePoolConfig = createDefaultServicePlatformNodePoolConfig(serviceConfig);
const searchProviderRuntimeConfig = serviceDeployHints.searchConfig?.deploy?.providerRuntime ?? {};
return {
cluster: {
deletion_protection: clusterConfig.deletionProtection,
kubernetes_version: clusterConfig.kubernetesVersion,
location: clusterConfig.location,
name: clusterConfig.name,
private_cluster: clusterConfig.privateCluster,
release_channel: clusterConfig.releaseChannel
},
contract_version: SERVICE_PLATFORM_TERRAFORM_CONTRACT_VERSION,
environment: context.environment ?? null,
network: normalizeOptionalString(searchProviderRuntimeConfig.network) ?? 'default',
node_pool: {
disk_size_gb: nodePoolConfig.diskSizeGb,
disk_type: nodePoolConfig.diskType,
machine_type: nodePoolConfig.machineType,
max_node_count: nodePoolConfig.maxNodeCount,
min_node_count: nodePoolConfig.minNodeCount,
name: nodePoolConfig.name,
node_labels: nodePoolConfig.nodeLabels,
node_taints: nodePoolConfig.nodeTaints,
service_account_email: nodePoolConfig.serviceAccountEmail,
spot: nodePoolConfig.spot
},
project_id: context.projectId,
resource_labels: {
...createServiceResourceLabels(context, serviceName),
'atlas-platform': 'gke-platform',
'atlas-runtime': 'platform'
},
subnetwork: normalizeOptionalString(searchProviderRuntimeConfig.subnetwork) ?? null
};
};
export const getServiceTerraformArtifactPath = (projectId, serviceName, cwd = process.cwd()) => path.join(getAtlasGeneratedTerraformDir(cwd), 'services', `service.${serviceName}.${projectId}.tfvars.json`);
export const getServicePlatformTerraformArtifactPath = (projectId, cwd = process.cwd()) => path.join(getAtlasGeneratedTerraformDir(cwd), 'services', `platform.gke.${projectId}.tfvars.json`);
export const writeServiceTerraformArtifact = (context, serviceName, serviceConfig, options = {}, cwd = process.cwd()) => {
const writeJsonFileImpl = options.writeJsonFile ?? writeJsonFile;
const filePath = getServiceTerraformArtifactPath(context.projectId, serviceName, cwd);
const payload = createServiceTerraformPayload(context, serviceName, serviceConfig, options);
writeJsonFileImpl(filePath, payload);
return {
contractVersion: payload.contract_version,
filePath,
payload
};
};
export const writeServicePlatformTerraformArtifact = (context, serviceName, serviceConfig, serviceDeployHints = {}, options = {}, cwd = process.cwd()) => {
const writeJsonFileImpl = options.writeJsonFile ?? writeJsonFile;
const filePath = getServicePlatformTerraformArtifactPath(context.projectId, cwd);
const payload = createServicePlatformTerraformPayload(context, serviceName, serviceConfig, serviceDeployHints);
writeJsonFileImpl(filePath, payload);
return {
contractVersion: payload.contract_version,
filePath,
payload
};
};
export const runServicePlatformTerraformWorkflow = async (context, serviceName, serviceConfig, serviceDeployHints, terraformArtifact, options = {}, dependencies = {}, cwd = process.cwd()) => runServicePlatformTerraformWorkflowImpl(context, serviceName, serviceConfig, serviceDeployHints, terraformArtifact, options, {
...dependencies,
ensureServicePlatformTerraformRoot: dependencies.ensureServicePlatformTerraformRoot ?? ensureServicePlatformTerraformRoot,
terraformInputVariableName: SERVICE_PLATFORM_TERRAFORM_INPUT_VARIABLE
}, cwd);
export const readServicePlatformTerraformOutputs = async (context, serviceName, serviceConfig, serviceDeployHints, dependencies = {}, cwd = process.cwd()) => readServicePlatformTerraformOutputsImpl(context, serviceName, serviceConfig, serviceDeployHints, {
...dependencies,
ensureServicePlatformTerraformRoot: dependencies.ensureServicePlatformTerraformRoot ?? ensureServicePlatformTerraformRoot
}, cwd);
export const runServiceTerraformWorkflow = async (context, serviceName, serviceConfig, serviceDeployHints, terraformArtifact, options = {}, dependencies = {}, cwd = process.cwd()) => runServiceTerraformWorkflowImpl(context, serviceName, serviceConfig, serviceDeployHints, terraformArtifact, options, {
...dependencies,
ensureServiceTerraformRoot: dependencies.ensureServiceTerraformRoot ?? ensureServiceTerraformRoot,
terraformInputVariableName: SERVICE_TERRAFORM_INPUT_VARIABLE
}, cwd);
export const readServiceTerraformOutputs = async (context, serviceName, serviceConfig, serviceDeployHints, dependencies = {}, cwd = process.cwd()) => readServiceTerraformOutputsImpl(context, serviceName, serviceConfig, serviceDeployHints, {
...dependencies,
ensureServiceTerraformRoot: dependencies.ensureServiceTerraformRoot ?? ensureServiceTerraformRoot
}, cwd);
export const createServiceSecretPayload = async (serviceName, serviceConfig, outputs, dependencies = {}) => {
const service = getService(serviceName);
return await service.createSecretPayload(outputs, serviceConfig, dependencies);
};
export const syncServiceAccessSecret = async (context, serviceName, serviceConfig, terraformOutputs, dependencies = {}) => {
const upsertSecretImpl = dependencies.upsertSecret ?? upsertSecret;
const secretName = normalizeOptionalString(serviceConfig.accessSecret) ?? getService(serviceName).createDefaultConfigSection().accessSecret;
const payload = await createServiceSecretPayload(serviceName, serviceConfig, terraformOutputs, dependencies);
const result = await upsertSecretImpl(secretName, payload, context.projectId);
const payloadData = (() => {
try {
return JSON.parse(payload);
} catch {
return null;
}
})();
return {
adminEmail: payloadData?.adminEmail ?? terraformOutputs.admin_email ?? null,
ingressIpAddress: terraformOutputs.ingress_ip_address ?? null,
secretName,
status: result.status,
url: payloadData?.url ?? terraformOutputs.url ?? null,
warnings: []
};
};
export const logServiceDeploySummary = (context, serviceName, serviceConfig, clusterTarget, terraformWorkflow, terraformArtifact, secretSync, loggerImpl = logger) => {
loggerImpl.summary('Service deployment', [{
label: 'Project',
value: context.projectId
}, context.environment ? {
label: 'Environment',
value: context.environment
} : null, {
label: 'Service',
value: serviceName
}, {
label: 'Namespace',
value: serviceConfig.namespace
}, {
label: 'Host',
value: serviceConfig.host
}, {
label: 'GKE cluster',
value: clusterTarget.clusterName
}, {
label: 'GKE location',
value: clusterTarget.clusterLocation
}, secretSync?.url ? {
label: 'URL',
value: secretSync.url
} : null, secretSync ? {
label: 'Access secret',
value: `${secretSync.secretName} (${secretSync.status}).`
} : null]);
if (secretSync?.ingressIpAddress) {
loggerImpl.summary('DNS setup', [{
label: 'Record type',
value: 'A'
}, {
label: 'Host',
value: serviceConfig.host
}, {
label: 'Target IP',
value: secretSync.ingressIpAddress
}, {
label: 'Instruction',
value: `Point ${serviceConfig.host} to ${secretSync.ingressIpAddress}.`
}]);
}
if (secretSync) {
loggerImpl.summary('Login', [secretSync.url ? {
label: 'URL',
value: secretSync.url
} : null, secretSync.adminEmail ?? serviceConfig.adminEmail ? {
label: 'Username',
value: secretSync.adminEmail ?? serviceConfig.adminEmail
} : null, {
label: 'Password source',
value: secretSync.status === 'dry-run' ? `${secretSync.secretName} (after apply).` : `${secretSync.secretName} (read the "password" field).`
}]);
}
loggerImpl.summary('Terraform workspace', [{
label: 'Terraform root',
value: terraformWorkflow?.rootPath ?? 'skipped'
}, terraformArtifact ? {
label: 'Terraform input',
value: `${SERVICE_TERRAFORM_INPUT_VARIABLE}=${terraformArtifact.filePath}`
} : null, terraformWorkflow?.moduleSource ? {
label: 'Terraform module source',
value: terraformWorkflow.moduleSource
} : null]);
};