UNPKG

@puls-atlas/cli

Version:

The Puls Atlas CLI tool for managing Atlas projects

1,015 lines (1,014 loc) 77.3 kB
import { randomBytes } from 'node:crypto'; import fs from 'fs'; import path from 'path'; import inquirer from 'inquirer'; import { isEqual } from 'es-toolkit'; import { isPlainObject } from 'es-toolkit/compat'; import { omit } from 'es-toolkit/object'; import * as features from '../../utils/feature.js'; import { readJsonFile, writeJsonFile } from '../../utils/file.js'; import { upsertSecret } from '../../utils/secrets.js'; import { logger } from '../../utils/logger.js'; import { getAtlasGeneratedTerraformDir, resolveRootPath } from '../../utils/atlas.js'; import { getSearchProvider } from './providers/index.js'; import { resolveSearchCloudRunDeployConfig } from './planning.js'; import { dedupeMessages, normalizeOptionalString, spreadIf } from '../../utils/value.js'; import { isGcloudResourceNotFoundError, parseGcloudJsonOutput, runGcloudFileCommand } from '../../utils/gcloud.js'; import { ensureTerraformWorkspace, createTerraformStringVariableArgument, runTerraformCommand } from '../../utils/terraform.js'; import { loadSearchProviderGkeTerraformRootTemplateFiles, loadSearchProviderPlatformTerraformRootTemplateFiles, loadSearchProviderTerraformRootTemplateFiles, resolveSiblingTerraformModuleSource, renderSearchTerraformRootTemplateFiles } from './terraformRootTemplates.js'; import { DEFAULT_SEARCH_TERRAFORM_MODULE_RELEASE_TAG, DEFAULT_SEARCH_TERRAFORM_ROOT_DIR, resolveSearchCloudRunServiceAccountEmail } from './config/searchConfig.js'; const TERRAFORM_HEARTBEAT_INTERVAL_MS = 30_000; const DEFAULT_CLOUD_RUN_PRIVATE_EGRESS = 'private-ranges-only'; const IAP_TCP_FORWARDING_SOURCE_RANGE = '35.235.240.0/20'; const DEFAULT_PRIVATE_INGRESS_SOURCE_RANGES = ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', IAP_TCP_FORWARDING_SOURCE_RANGE]; const PROVIDER_TERRAFORM_IMPORT_ALREADY_MANAGED_PATTERN = /already managed by Terraform/iu; const PROVIDER_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 resolveSearchProviderTerraformWorkspaceName = terraformArtifact => { const workspaceName = normalizeOptionalString(terraformArtifact?.payload?.project_id); if (workspaceName) { return workspaceName; } throw new Error('Atlas search provider Terraform input must define payload.project_id before Terraform can select a workspace.'); }; export const SEARCH_PROVIDER_TERRAFORM_CONTRACT_VERSION = 1; export const SEARCH_PROVIDER_TERRAFORM_INPUT_VARIABLE = 'atlas_search_provider_tfvars_path'; export const SEARCH_PROVIDER_PLATFORM_TERRAFORM_CONTRACT_VERSION = 1; export const SEARCH_PROVIDER_PLATFORM_TERRAFORM_INPUT_VARIABLE = 'atlas_search_provider_platform_tfvars_path'; export const DEFAULT_SEARCH_PROVIDER_TERRAFORM_ROOT_DIR = 'services/search/terraform/provider'; export const DEFAULT_SEARCH_PROVIDER_PLATFORM_TERRAFORM_ROOT_DIR = 'services/search/terraform/provider-platform'; export const DEFAULT_SEARCH_PROVIDER_TERRAFORM_MODULE_SOURCE = `git::https://github.com/limebooth/atlas-terraform-modules.git//search?ref=${DEFAULT_SEARCH_TERRAFORM_MODULE_RELEASE_TAG}`; const DEFAULT_SEARCH_PROVIDER_KUBECONFIG_ROOT_DIR = path.join('.atlas', 'kubeconfig'); const EMPTY_KUBECONFIG_CONTENT = `apiVersion: v1 kind: Config preferences: {} clusters: [] contexts: [] current-context: "" users: [] `; const PROVIDER_PLATFORM_CHOICES = [{ name: 'Compute Engine instance', value: 'compute' }, { name: 'GKE workload', value: 'gke' }]; const GKE_CLUSTER_STRATEGY_CHOICES = [{ name: 'Use an existing GKE cluster', value: 'existing' }, { name: 'Create a managed GKE cluster first', value: 'managed' }]; const COMPUTE_CONFIG_FIELD_MAPPINGS = [['machineType', 'machine_type'], ['bootDiskSizeGb', 'boot_disk_size_gb'], ['bootDiskImage', 'boot_disk_image'], ['dataDiskSizeGb', 'data_disk_size_gb'], ['dataDiskType', 'data_disk_type'], ['dataMountPath', 'data_mount_path'], ['assignPublicIp', 'assign_public_ip'], ['createNatGateway', 'create_nat_gateway'], ['createFirewallRule', 'create_firewall_rule'], ['ingressSourceRanges', 'ingress_source_ranges'], ['tags', 'tags'], ['metadata', 'metadata']]; const GKE_CONFIG_FIELD_MAPPINGS = [['namespace', 'namespace'], ['createNamespace', 'create_namespace'], ['replicas', 'replicas'], ['serviceType', 'service_type'], ['storageClassName', 'storage_class_name'], ['storageSize', 'storage_size'], ['nodeSelector', 'node_selector'], ['annotations', 'annotations'], ['resourceRequests', 'resource_requests'], ['resourceLimits', 'resource_limits']]; const normalizeKubeconfigPathSegment = value => String(value).trim().replace(/[^A-Za-z0-9._-]+/gu, '-').replace(/-{2,}/gu, '-').replace(/^-+|-+$/gu, ''); const createKubernetesHost = clusterEndpoint => clusterEndpoint.startsWith('http') ? clusterEndpoint : `https://${clusterEndpoint}`; const resolveSearchProviderClusterTarget = (providerRuntimeConfig, platformOutputs = {}) => ({ clusterLocation: normalizeOptionalString(platformOutputs.cluster_location) ?? normalizeOptionalString(providerRuntimeConfig.gke?.cluster?.location), clusterName: normalizeOptionalString(platformOutputs.cluster_name) ?? normalizeOptionalString(providerRuntimeConfig.gke?.cluster?.name) }); const resolveSearchProviderKubeconfigPath = (context, clusterTarget, cwd = process.cwd(), options = {}) => { const configuredPath = normalizeOptionalString(options.kubeconfigPath); if (configuredPath) { return resolveRootPath(configuredPath, cwd); } const fileName = [context.projectId, clusterTarget.clusterLocation, clusterTarget.clusterName].map(normalizeKubeconfigPathSegment).filter(Boolean).join('.'); return resolveRootPath(path.join(DEFAULT_SEARCH_PROVIDER_KUBECONFIG_ROOT_DIR, `${fileName || 'search-provider'}.yaml`), cwd); }; const ensureSearchProviderKubeconfigFile = (kubeconfigPath, dependencies = {}) => { if (!kubeconfigPath) { return null; } const { existsSyncImpl = fs.existsSync, mkdirSyncImpl = fs.mkdirSync, writeFileSyncImpl = fs.writeFileSync } = dependencies; mkdirSyncImpl(path.dirname(kubeconfigPath), { recursive: true }); if (!existsSyncImpl(kubeconfigPath)) { writeFileSyncImpl(kubeconfigPath, EMPTY_KUBECONFIG_CONTENT); } return kubeconfigPath; }; const formatDuration = elapsedMs => `${Math.max(1, Math.round(elapsedMs / 1000))}s`; const describeTerraformStep = args => `terraform ${args[0]}`; const getTerraformStepGuardMessage = (args, providerRuntimeConfig) => { switch (args[0]) { case 'init': return 'Terraform init may still be downloading modules or providers. ' + 'First runs and restricted networks can make this step noticeably slower.'; case 'apply': case 'plan': case 'destroy': if (providerRuntimeConfig.platform === 'compute') { return 'Compute Engine provisioning can take several minutes while Google Cloud creates the VM, ' + 'persistent disk and firewall, then boots the instance.'; } if (providerRuntimeConfig.platform === 'gke') { return 'GKE-backed provider changes can take several minutes while Terraform waits on cluster-side resources ' + 'and Kubernetes provider calls.'; } return 'Terraform is still applying provider infrastructure changes.'; case 'output': return 'Terraform is still reading provider outputs from the local state.'; default: return 'Terraform is still running.'; } }; const createTerraformLifecycleHooks = (loggerImpl, providerRuntimeConfig) => ({ onHeartbeat: ({ args, elapsedMs }) => { loggerImpl.warning(`${describeTerraformStep(args)} is still running after ${formatDuration(elapsedMs)}. ` + `${getTerraformStepGuardMessage(args, providerRuntimeConfig)}`); } }); const createSearchProviderTerraformContext = (context, providerRuntimeConfig) => ({ ...context.config, deploy: { ...(context.config.deploy ?? {}), providerRuntime: providerRuntimeConfig } }); const resolveProviderTerraformLifecycleHooks = (dependencies, providerRuntimeConfig) => dependencies.terraformLifecycleHooks ?? createTerraformLifecycleHooks(dependencies.logger ?? logger, providerRuntimeConfig); const runTerraformStepWithLogging = async (args, workflowSummary, runTerraformCommandImpl, terraformLifecycleHooks, options = {}) => { const { logger: loggerOverride, logCompletion = true, logStart = true, ...terraformCommandOptions } = options; const loggerImpl = loggerOverride ?? logger; const startedAt = Date.now(); if (logStart) { loggerImpl.info(`Starting ${describeTerraformStep(args)} in ${workflowSummary.rootPath}${terraformCommandOptions.captureOutput ? ' (capturing output)' : ''}.`); } const result = await runTerraformCommandImpl(args, { cwd: workflowSummary.rootPath, heartbeatIntervalMs: TERRAFORM_HEARTBEAT_INTERVAL_MS, ...terraformLifecycleHooks, ...terraformCommandOptions }); if (logCompletion) { loggerImpl.info(`${describeTerraformStep(args)} completed in ${formatDuration(Date.now() - startedAt)}.`); } return result; }; const normalizePlatform = platform => { const normalized = normalizeOptionalString(platform)?.toLowerCase(); if (!normalized) { return null; } if (!['compute', 'gke'].includes(normalized)) { throw new Error('Atlas search provider deploy supports only --platform compute or --platform gke.'); } return normalized; }; const getProviderRuntimeConfig = searchConfig => searchConfig?.deploy?.providerRuntime ?? {}; const normalizeStringArray = value => { if (!Array.isArray(value)) { return []; } return value.map(entry => normalizeOptionalString(entry)).filter(Boolean); }; const ensureIapIngressSourceRange = ingressSourceRanges => { const normalizedIngressSourceRanges = normalizeStringArray(ingressSourceRanges); if (normalizedIngressSourceRanges.includes(IAP_TCP_FORWARDING_SOURCE_RANGE)) { return normalizedIngressSourceRanges; } return [...normalizedIngressSourceRanges, IAP_TCP_FORWARDING_SOURCE_RANGE]; }; const hasOwnProperty = (object, propertyName) => Object.prototype.hasOwnProperty.call(object, propertyName); const assignMappedProperties = (source, fieldMappings) => { const mappedProperties = {}; for (const [sourceKey, targetKey] of fieldMappings) { if (!hasOwnProperty(source, sourceKey)) { continue; } const value = source[sourceKey]; if (value === undefined) { continue; } mappedProperties[targetKey] = value; } return mappedProperties; }; const createDefaultPrivateComputeConfig = currentConfig => ({ assignPublicIp: currentConfig.compute?.assignPublicIp ?? false, createNatGateway: currentConfig.compute?.createNatGateway ?? true, createFirewallRule: currentConfig.compute?.createFirewallRule ?? true, ingressSourceRanges: (() => { const ingressSourceRanges = normalizeStringArray(currentConfig.compute?.ingressSourceRanges); return ingressSourceRanges.length > 0 ? ensureIapIngressSourceRange(ingressSourceRanges) : [...DEFAULT_PRIVATE_INGRESS_SOURCE_RANGES]; })() }); const createDefaultGkeClusterName = providerRuntimeConfig => `${providerRuntimeConfig.namePrefix ?? 'atlas-search'}-gke`; const createDefaultGkeNodePoolName = providerRuntimeConfig => `${providerRuntimeConfig.namePrefix ?? 'atlas-search'}-workload`; const createDefaultGkeNodePoolMachineType = providerName => providerName === 'elasticsearch' ? 'e2-standard-4' : 'e2-standard-2'; const createDefaultGkeClusterConfig = (context, providerRuntimeConfig = {}, currentClusterConfig = {}) => ({ create: currentClusterConfig.create ?? false, deletionProtection: currentClusterConfig.deletionProtection ?? false, location: currentClusterConfig.location ?? resolveSearchCloudRunDeployConfig(context).region, name: currentClusterConfig.name ?? createDefaultGkeClusterName(providerRuntimeConfig), kubernetesVersion: currentClusterConfig.kubernetesVersion ?? null, privateCluster: currentClusterConfig.privateCluster ?? false, releaseChannel: currentClusterConfig.releaseChannel ?? 'REGULAR' }); const createDefaultGkeNodePoolConfig = (providerName, providerRuntimeConfig = {}, currentNodePoolConfig = {}) => ({ diskSizeGb: currentNodePoolConfig.diskSizeGb ?? 100, diskType: currentNodePoolConfig.diskType ?? 'pd-balanced', machineType: currentNodePoolConfig.machineType ?? createDefaultGkeNodePoolMachineType(providerName), maxNodeCount: currentNodePoolConfig.maxNodeCount ?? 3, minNodeCount: currentNodePoolConfig.minNodeCount ?? 1, name: currentNodePoolConfig.name ?? createDefaultGkeNodePoolName(providerRuntimeConfig), nodeLabels: currentNodePoolConfig.nodeLabels ?? {}, nodeTaints: currentNodePoolConfig.nodeTaints ?? [], serviceAccountEmail: currentNodePoolConfig.serviceAccountEmail ?? null, spot: currentNodePoolConfig.spot ?? false }); const isManagedGkeCluster = providerRuntimeConfig => providerRuntimeConfig.platform === 'gke' && providerRuntimeConfig.gke?.cluster?.create === true; const normalizeTerraformRootDir = rootDir => (normalizeOptionalString(rootDir) ?? DEFAULT_SEARCH_TERRAFORM_ROOT_DIR).replaceAll('\\', '/').replace(/\/+$/u, ''); const getDefaultSearchProviderTerraformRootDir = searchConfig => `${normalizeTerraformRootDir(searchConfig?.deploy?.terraform?.rootDir)}/provider`; const getDefaultSearchProviderPlatformTerraformRootDir = searchConfig => `${normalizeTerraformRootDir(searchConfig?.deploy?.terraform?.rootDir)}/provider-platform`; const createManagedCloudRunVpcAccessConfig = providerRuntimeConfig => { if (providerRuntimeConfig.platform !== 'compute' || providerRuntimeConfig.compute?.assignPublicIp === true) { return null; } return { egress: DEFAULT_CLOUD_RUN_PRIVATE_EGRESS, network: providerRuntimeConfig.network ?? 'default', subnetwork: providerRuntimeConfig.subnetwork ?? null }; }; export const resolveSearchProviderTerraformConfig = searchConfig => { const providerRuntimeConfig = getProviderRuntimeConfig(searchConfig); return { moduleSource: providerRuntimeConfig.moduleSource ?? DEFAULT_SEARCH_PROVIDER_TERRAFORM_MODULE_SOURCE, rootDir: providerRuntimeConfig.rootDir ?? getDefaultSearchProviderTerraformRootDir(searchConfig) }; }; export const resolveSearchProviderTerraformRootPath = (searchConfig, cwd = process.cwd()) => resolveRootPath(resolveSearchProviderTerraformConfig(searchConfig).rootDir, cwd); export const createSearchProviderTerraformWorkflowSummary = (context, providerRuntimeConfig, cwd = process.cwd()) => { const terraformConfig = resolveSearchProviderTerraformConfig({ deploy: { providerRuntime: providerRuntimeConfig } }); return { managedResources: [{ description: `Self-hosted ${context.config.provider} runtime managed through the Atlas search provider Terraform workflow.`, kind: providerRuntimeConfig.platform ?? 'provider-runtime', name: `${providerRuntimeConfig.namePrefix ?? 'atlas-search'}-${context.config.provider}` }], moduleSource: terraformConfig.moduleSource, rootDir: terraformConfig.rootDir, rootPath: resolveRootPath(terraformConfig.rootDir, cwd) }; }; export const ensureSearchProviderTerraformRoot = async (searchConfig, cwd = process.cwd(), options = {}) => { const { existsSyncImpl = fs.existsSync, mkdirSyncImpl = fs.mkdirSync, readFileSyncImpl = fs.readFileSync, templateDirectory, writeFileSyncImpl = fs.writeFileSync } = options; const providerRuntimeConfig = getProviderRuntimeConfig(searchConfig); const workflowSummary = createSearchProviderTerraformWorkflowSummary({ config: { provider: searchConfig.provider } }, providerRuntimeConfig, cwd); const loadTemplateFiles = providerRuntimeConfig.platform === 'gke' ? loadSearchProviderGkeTerraformRootTemplateFiles : loadSearchProviderTerraformRootTemplateFiles; const resolvedTemplateDirectory = providerRuntimeConfig.platform === 'gke' ? options.gkeTemplateDirectory ?? templateDirectory : templateDirectory; const templateFiles = await loadTemplateFiles(workflowSummary.moduleSource, cwd, { ...options, templateDirectory: resolvedTemplateDirectory }); const rootFiles = renderSearchTerraformRootTemplateFiles(templateFiles, { __ATLAS_SEARCH_PROVIDER_TERRAFORM_CONTRACT_VERSION__: SEARCH_PROVIDER_TERRAFORM_CONTRACT_VERSION, __ATLAS_SEARCH_PROVIDER_TERRAFORM_INPUT_VARIABLE__: SEARCH_PROVIDER_TERRAFORM_INPUT_VARIABLE, __ATLAS_SEARCH_PROVIDER_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 }; }; const createRandomHex = randomBytesImpl => randomBytesImpl(24).toString('hex'); const createRandomPassword = randomBytesImpl => randomBytesImpl(24).toString('base64url'); const createSearchProviderCredentialSeed = (providerName, providerAccess, randomBytesImpl) => { if (providerName === 'typesense') { return { adminApiKey: providerAccess.adminApiKey ?? createRandomHex(randomBytesImpl), host: providerAccess.host ?? null }; } return { apiKey: null, node: providerAccess.node ?? null, password: providerAccess.password ?? createRandomPassword(randomBytesImpl), username: providerAccess.username ?? 'elastic' }; }; const createProviderRuntimeConfigUpdate = (currentConfig, update) => ({ ...currentConfig, ...update, compute: update.compute ? { ...(currentConfig.compute ?? {}), ...update.compute } : currentConfig.compute, gke: update.gke ? { ...(currentConfig.gke ?? {}), ...update.gke, cluster: update.gke.cluster ? { ...(currentConfig.gke?.cluster ?? {}), ...update.gke.cluster } : currentConfig.gke?.cluster, nodePool: update.gke.nodePool ? { ...(currentConfig.gke?.nodePool ?? {}), ...update.gke.nodePool } : currentConfig.gke?.nodePool } : currentConfig.gke }); const normalizeCloudRunVpcAccessValue = vpcAccess => { if (!isPlainObject(vpcAccess)) { return null; } return { egress: normalizeOptionalString(vpcAccess.egress), network: normalizeOptionalString(vpcAccess.network), subnetwork: normalizeOptionalString(vpcAccess.subnetwork) }; }; const areCloudRunVpcAccessConfigsEqual = (left, right) => isEqual(normalizeCloudRunVpcAccessValue(left), normalizeCloudRunVpcAccessValue(right)); const resolveSearchProviderGkeSelection = async (context, currentConfig, options, dependencies) => { const promptImpl = dependencies.prompt ?? inquirer.prompt; const currentGkeConfig = currentConfig.gke ?? {}; const currentClusterConfig = currentGkeConfig.cluster ?? {}; const currentNodePoolConfig = currentGkeConfig.nodePool ?? {}; let clusterCreate = currentClusterConfig.create; let clusterName = normalizeOptionalString(options.clusterName ?? currentClusterConfig.name); if (options.createCluster === true) { clusterCreate = true; } if (clusterCreate !== true && clusterCreate !== false) { const answer = await promptImpl([{ choices: GKE_CLUSTER_STRATEGY_CHOICES, default: currentClusterConfig.name ? 'existing' : 'managed', message: 'How should Atlas obtain the GKE cluster for the provider runtime?', name: 'gkeClusterStrategy', type: 'select' }]); clusterCreate = answer.gkeClusterStrategy === 'managed'; } const clusterDefaults = createDefaultGkeClusterConfig(context, currentConfig, { ...currentClusterConfig, create: clusterCreate }); const nodePoolDefaults = createDefaultGkeNodePoolConfig(context.config.provider, currentConfig, currentNodePoolConfig); const clusterLocation = normalizeOptionalString(options.clusterLocation ?? currentClusterConfig.location) ?? clusterDefaults.location; if (clusterCreate && !clusterName) { const answer = await promptImpl([{ default: clusterDefaults.name, message: 'Which GKE cluster name should Atlas manage?', name: 'clusterName', type: 'input' }]); clusterName = normalizeOptionalString(answer.clusterName); } return { gke: { ...currentGkeConfig, cluster: { ...clusterDefaults, ...(clusterName ? { name: clusterName } : {}), ...(clusterLocation ? { location: clusterLocation } : {}) }, nodePool: clusterCreate ? { ...nodePoolDefaults } : currentNodePoolConfig } }; }; export const resolveSearchProviderRuntimeSelection = async (context, options = {}, dependencies = {}) => { const promptImpl = dependencies.prompt ?? inquirer.prompt; const currentConfig = getProviderRuntimeConfig(context.config); let platform = normalizePlatform(options.platform ?? currentConfig.platform); if (!platform) { const answer = await promptImpl([{ choices: PROVIDER_PLATFORM_CHOICES, message: `How should Atlas deploy the ${context.config.provider} provider runtime?`, name: 'platform', type: 'select' }]); platform = normalizePlatform(answer.platform); } const selection = { platform }; if (platform === 'compute') { let zone = normalizeOptionalString(options.zone ?? currentConfig.compute?.zone); if (!zone) { const answer = await promptImpl([{ default: 'europe-west1-b', message: 'Which Compute Engine zone should host the search provider runtime?', name: 'zone', type: 'input' }]); zone = normalizeOptionalString(answer.zone); } if (!zone) { throw new Error('Atlas search provider deploy requires a Compute Engine zone when platform=compute.'); } selection.compute = { ...createDefaultPrivateComputeConfig(currentConfig), zone }; } else if (platform === 'gke') { Object.assign(selection, await resolveSearchProviderGkeSelection(context, currentConfig, options, { prompt: promptImpl })); } return createProviderRuntimeConfigUpdate(currentConfig, selection); }; export const persistSearchProviderRuntimeConfig = (context, providerRuntimeConfig, dependencies = {}) => { const readJsonFileImpl = dependencies.readJsonFile ?? readJsonFile; const writeJsonFileImpl = dependencies.writeJsonFile ?? writeJsonFile; const currentRootConfig = readJsonFileImpl(context.configPath); const currentResolvedConfig = context.config ?? currentRootConfig; const currentCloudRunConfig = currentResolvedConfig.deploy?.cloudRun ?? {}; const currentCloudRunVpcAccess = currentCloudRunConfig.vpcAccess; const previousProviderRuntimeConfig = currentResolvedConfig.deploy?.providerRuntime ?? {}; const managedCloudRunVpcAccess = createManagedCloudRunVpcAccessConfig(providerRuntimeConfig); const previousManagedCloudRunVpcAccess = createManagedCloudRunVpcAccessConfig(previousProviderRuntimeConfig); let nextCloudRunConfig = { ...currentCloudRunConfig }; if (managedCloudRunVpcAccess) { if (currentCloudRunVpcAccess === undefined || areCloudRunVpcAccessConfigsEqual(currentCloudRunVpcAccess, previousManagedCloudRunVpcAccess) || areCloudRunVpcAccessConfigsEqual(currentCloudRunVpcAccess, managedCloudRunVpcAccess)) { nextCloudRunConfig.vpcAccess = managedCloudRunVpcAccess; } } else if (previousManagedCloudRunVpcAccess && areCloudRunVpcAccessConfigsEqual(currentCloudRunVpcAccess, previousManagedCloudRunVpcAccess)) { nextCloudRunConfig = omit(nextCloudRunConfig, ['vpcAccess']); } let nextRootConfig; if (currentRootConfig.projects?.[context.projectId]) { nextRootConfig = { ...currentRootConfig, projects: { ...(currentRootConfig.projects ?? {}), [context.projectId]: { ...(currentRootConfig.projects?.[context.projectId] ?? {}), ...(context.environment ? { environment: context.environment } : {}), deploy: { ...(currentRootConfig.projects?.[context.projectId]?.deploy ?? {}), cloudRun: nextCloudRunConfig, providerRuntime: providerRuntimeConfig } } } }; } else { nextRootConfig = { ...currentRootConfig, deploy: { ...(currentRootConfig.deploy ?? {}), cloudRun: nextCloudRunConfig, providerRuntime: providerRuntimeConfig } }; } if (!isEqual(currentRootConfig, nextRootConfig)) { writeJsonFileImpl(context.configPath, nextRootConfig); } return nextRootConfig; }; export const resolveSearchProviderPlatformTerraformConfig = searchConfig => { const providerRuntimeConfig = getProviderRuntimeConfig(searchConfig); const providerModuleSource = providerRuntimeConfig.moduleSource ?? DEFAULT_SEARCH_PROVIDER_TERRAFORM_MODULE_SOURCE; return { clusterModuleSource: resolveSiblingTerraformModuleSource(providerModuleSource, 'platform/gke-cluster'), nodePoolModuleSource: resolveSiblingTerraformModuleSource(providerModuleSource, 'platform/gke-node-pool'), rootDir: providerRuntimeConfig.gke?.platformRootDir ?? getDefaultSearchProviderPlatformTerraformRootDir(searchConfig) }; }; export const resolveSearchProviderPlatformTerraformRootPath = (searchConfig, cwd = process.cwd()) => resolveRootPath(resolveSearchProviderPlatformTerraformConfig(searchConfig).rootDir, cwd); export const createSearchProviderPlatformTerraformWorkflowSummary = (_context, providerRuntimeConfig, cwd = process.cwd()) => { const terraformConfig = resolveSearchProviderPlatformTerraformConfig({ deploy: { providerRuntime: providerRuntimeConfig } }); const clusterName = providerRuntimeConfig.gke?.cluster?.name ?? createDefaultGkeClusterName(providerRuntimeConfig); const nodePoolName = providerRuntimeConfig.gke?.nodePool?.name ?? createDefaultGkeNodePoolName(providerRuntimeConfig); return { clusterModuleSource: terraformConfig.clusterModuleSource, managedResources: [{ description: 'Managed GKE cluster foundation for the Atlas search provider runtime.', kind: 'gke-cluster', name: clusterName }, { description: 'Managed GKE node pool that hosts the Atlas search provider workload.', kind: 'gke-node-pool', name: nodePoolName }], nodePoolModuleSource: terraformConfig.nodePoolModuleSource, rootDir: terraformConfig.rootDir, rootPath: resolveRootPath(terraformConfig.rootDir, cwd) }; }; export const ensureSearchProviderPlatformTerraformRoot = async (searchConfig, cwd = process.cwd(), options = {}) => { const { existsSyncImpl = fs.existsSync, mkdirSyncImpl = fs.mkdirSync, readFileSyncImpl = fs.readFileSync, templateDirectory, writeFileSyncImpl = fs.writeFileSync } = options; const workflowSummary = createSearchProviderPlatformTerraformWorkflowSummary({ config: { provider: searchConfig.provider } }, getProviderRuntimeConfig(searchConfig), cwd); const templateFiles = await loadSearchProviderPlatformTerraformRootTemplateFiles(getProviderRuntimeConfig(searchConfig).moduleSource ?? DEFAULT_SEARCH_PROVIDER_TERRAFORM_MODULE_SOURCE, cwd, { ...options, templateDirectory }); const rootFiles = renderSearchTerraformRootTemplateFiles(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__: SEARCH_PROVIDER_PLATFORM_TERRAFORM_CONTRACT_VERSION, __ATLAS_SEARCH_PROVIDER_PLATFORM_TERRAFORM_INPUT_VARIABLE__: SEARCH_PROVIDER_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 }; }; const createSearchProviderResourceLabels = (context, platform) => ({ 'atlas-component': 'search', ...spreadIf(context.environment, { 'atlas-environment': context.environment }), 'atlas-managed-by': 'terraform', 'atlas-platform': platform, 'atlas-project': context.projectId, 'atlas-provider': context.config.provider, 'atlas-runtime': 'provider' }); export const createSearchProviderPlatformTerraformPayload = (context, providerRuntimeConfig) => { if (!isManagedGkeCluster(providerRuntimeConfig)) { throw new Error('Atlas search provider platform payload requires platform=gke with gke.cluster.create=true.'); } const clusterConfig = createDefaultGkeClusterConfig(context, providerRuntimeConfig, providerRuntimeConfig.gke?.cluster ?? {}); const nodePoolConfig = createDefaultGkeNodePoolConfig(context.config.provider, providerRuntimeConfig, providerRuntimeConfig.gke?.nodePool ?? {}); const serviceAccountEmail = providerRuntimeConfig.serviceAccountEmail ?? resolveSearchCloudRunServiceAccountEmail(context.config, context.projectId); 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: SEARCH_PROVIDER_PLATFORM_TERRAFORM_CONTRACT_VERSION, environment: context.environment ?? null, name_prefix: providerRuntimeConfig.namePrefix ?? 'atlas-search', network: providerRuntimeConfig.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 ?? {}), ...(providerRuntimeConfig.gke?.nodeSelector ?? {}) }, node_taints: nodePoolConfig.nodeTaints, service_account_email: nodePoolConfig.serviceAccountEmail ?? serviceAccountEmail, spot: nodePoolConfig.spot }, project_id: context.projectId, provider: context.config.provider, region: resolveSearchCloudRunDeployConfig(context).region, resource_labels: createSearchProviderResourceLabels(context, 'gke-platform'), service_account_email: serviceAccountEmail, subnetwork: providerRuntimeConfig.subnetwork ?? null }; }; const mapComputeConfig = computeConfig => { if (!computeConfig) { return null; } return { zone: computeConfig.zone, ...assignMappedProperties(computeConfig, COMPUTE_CONFIG_FIELD_MAPPINGS) }; }; const mapGkeConfig = gkeConfig => { if (!gkeConfig) { return null; } return assignMappedProperties(gkeConfig, GKE_CONFIG_FIELD_MAPPINGS); }; const createSearchProviderTerraformConnectionPayload = terraformConnection => { if (!terraformConnection?.clusterEndpoint || !terraformConnection?.clusterCaCertificate) { return null; } return { cluster_ca_certificate: terraformConnection.clusterCaCertificate, host: createKubernetesHost(terraformConnection.clusterEndpoint) }; }; const createProviderRuntimeName = (context, providerRuntimeConfig) => `${providerRuntimeConfig.namePrefix ?? 'atlas-search'}-${context.config.provider}`; const isPrivateComputeProviderRuntime = providerRuntimeConfig => providerRuntimeConfig.platform === 'compute' && providerRuntimeConfig.compute?.assignPublicIp !== true; const getCommandErrorMessage = error => [error?.stderr?.toString?.() ?? '', error?.message ?? ''].filter(Boolean).join('\n'); const doesReferenceMatch = (reference, expectedValue, resourceType) => { const trimmedReference = normalizeOptionalString(reference); const trimmedExpectedValue = normalizeOptionalString(expectedValue); if (!trimmedReference || !trimmedExpectedValue) { return false; } return trimmedReference === trimmedExpectedValue || trimmedReference.endsWith(`/${resourceType}/${trimmedExpectedValue}`); }; const parseJsonCommandOutput = (output, description) => { try { return JSON.parse(output); } catch (error) { throw new Error(`Could not parse ${description}: ${error.message}`); } }; const listExistingRegionalCloudNats = (context, providerRuntimeConfig, runGcloudFileCommandImpl = runGcloudFileCommand) => { const network = providerRuntimeConfig.network ?? 'default'; const { region } = resolveSearchCloudRunDeployConfig(context); const routersOutput = runGcloudFileCommandImpl(['compute', 'routers', 'list', `--project=${context.projectId}`, '--format=json'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }); const routers = parseJsonCommandOutput(routersOutput, 'Cloud Router list').filter(router => doesReferenceMatch(router?.region, region, 'regions') && doesReferenceMatch(router?.network, network, 'networks')); return routers.flatMap(router => { const routerName = normalizeOptionalString(router?.name); if (!routerName) { return []; } const routerOutput = runGcloudFileCommandImpl(['compute', 'routers', 'describe', routerName, `--project=${context.projectId}`, `--region=${region}`, '--format=json'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }); const describedRouter = parseJsonCommandOutput(routerOutput, `Cloud Router ${routerName}`); return Array.isArray(describedRouter?.nats) ? describedRouter.nats.map(nat => normalizeOptionalString(nat?.name)).filter(Boolean).map(natName => ({ natName, routerName })) : []; }); }; const verifyExistingRegionalCloudNat = (context, region, candidate, runGcloudFileCommandImpl = runGcloudFileCommand) => { const natOutput = runGcloudFileCommandImpl(['compute', 'routers', 'nats', 'describe', candidate.natName, `--project=${context.projectId}`, `--router=${candidate.routerName}`, `--region=${region}`, '--format=json'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }); const describedNat = parseJsonCommandOutput(natOutput, `Cloud NAT ${candidate.natName} on router ${candidate.routerName}`); return normalizeOptionalString(describedNat?.name) === candidate.natName; }; const listVerifiedRegionalCloudNats = (context, providerRuntimeConfig, runGcloudFileCommandImpl = runGcloudFileCommand) => { const verifiedNats = []; const { region } = resolveSearchCloudRunDeployConfig(context); const candidates = listExistingRegionalCloudNats(context, providerRuntimeConfig, runGcloudFileCommandImpl); for (const candidate of candidates) { try { if (verifyExistingRegionalCloudNat(context, region, candidate, runGcloudFileCommandImpl)) { verifiedNats.push(candidate); } } catch (error) { if (isGcloudResourceNotFoundError(error)) { continue; } throw error; } } return verifiedNats; }; const createMissingNatBlockingIssue = (context, providerRuntimeConfig) => { const network = providerRuntimeConfig.network ?? 'default'; const { region } = resolveSearchCloudRunDeployConfig(context); return 'Private Compute Engine provider runtimes require outbound egress during startup, ' + 'but managed Cloud NAT is disabled and no reusable Cloud NAT was found. ' + `Project=${context.projectId}, network=${network}, region=${region}. ` + 'Enable deploy.providerRuntime.compute.createNatGateway or provision a Cloud NAT on a router for this region/network, then rerun "atlas search apply".'; }; const createUnverifiedNatBlockingIssue = (context, providerRuntimeConfig, error) => { const network = providerRuntimeConfig.network ?? 'default'; const { region } = resolveSearchCloudRunDeployConfig(context); return 'Private Compute Engine provider runtimes require outbound egress during startup, ' + 'but Atlas could not verify reusable Cloud NAT while managed NAT creation is disabled. ' + `Project=${context.projectId}, network=${network}, region=${region}. ${getCommandErrorMessage(error)} ` + 'Either enable deploy.providerRuntime.compute.createNatGateway or ensure Cloud NAT exists and your account ' + 'can inspect routers/nats, then rerun "atlas search apply".'; }; export const reuseExistingCloudNatForProviderRuntime = (context, providerRuntimeConfig, dependencies = {}) => { if (!isPrivateComputeProviderRuntime(providerRuntimeConfig) || providerRuntimeConfig.compute?.createNatGateway === false) { return { blockingIssues: [], providerRuntimeConfig, warnings: [] }; } const runGcloudFileCommandImpl = dependencies.runGcloudFileCommand ?? runGcloudFileCommand; const { region } = resolveSearchCloudRunDeployConfig(context); const network = providerRuntimeConfig.network ?? 'default'; try { const existingNats = listVerifiedRegionalCloudNats(context, providerRuntimeConfig, runGcloudFileCommandImpl); if (existingNats.length === 0) { return { blockingIssues: [], providerRuntimeConfig, warnings: [] }; } const [{ natName, routerName }] = existingNats; return { blockingIssues: [], providerRuntimeConfig: createProviderRuntimeConfigUpdate(providerRuntimeConfig, { compute: { createNatGateway: false } }), warnings: [`Detected existing Cloud NAT ${natName} on router ${routerName} for project ${context.projectId}, ` + `network ${network}, region ${region}. Atlas verified this NAT through "gcloud compute routers nats describe" ` + 'and will reuse that egress path, so Atlas will skip creating a managed NAT gateway for this provider runtime.'] }; } catch (error) { return { blockingIssues: [], providerRuntimeConfig, warnings: [`Could not inspect existing Cloud NAT configuration for project ${context.projectId}, ` + `network ${network}, region ${region}. Atlas will continue with managed NAT creation. ${getCommandErrorMessage(error)}`] }; } }; const resolvePrivateNatReuseForProviderRuntime = (context, providerRuntimeConfig, dependencies = {}) => { if (!isPrivateComputeProviderRuntime(providerRuntimeConfig)) { return { blockingIssues: [], persistedProviderRuntimeConfig: providerRuntimeConfig, runtimeProviderRuntimeConfig: providerRuntimeConfig, warnings: [] }; } if (providerRuntimeConfig.compute?.createNatGateway === false) { const runGcloudFileCommandImpl = dependencies.runGcloudFileCommand ?? runGcloudFileCommand; try { const existingNats = listVerifiedRegionalCloudNats(context, providerRuntimeConfig, runGcloudFileCommandImpl); if (existingNats.length === 0) { return { blockingIssues: [createMissingNatBlockingIssue(context, providerRuntimeConfig)], persistedProviderRuntimeConfig: providerRuntimeConfig, runtimeProviderRuntimeConfig: providerRuntimeConfig, warnings: [] }; } return { blockingIssues: [], persistedProviderRuntimeConfig: providerRuntimeConfig, runtimeProviderRuntimeConfig: providerRuntimeConfig, warnings: [] }; } catch (error) { return { blockingIssues: [createUnverifiedNatBlockingIssue(context, providerRuntimeConfig, error)], persistedProviderRuntimeConfig: providerRuntimeConfig, runtimeProviderRuntimeConfig: providerRuntimeConfig, warnings: [] }; } } const natReuse = reuseExistingCloudNatForProviderRuntime(context, providerRuntimeConfig, { runGcloudFileCommand: dependencies.runGcloudFileCommand }); return { blockingIssues: natReuse.blockingIssues ?? [], persistedProviderRuntimeConfig: providerRuntimeConfig, runtimeProviderRuntimeConfig: natReuse.providerRuntimeConfig, warnings: natReuse.warnings ?? [] }; }; export const createSearchProviderTerraformPayload = (context, providerRuntimeConfig, managedAccess, options = {}) => { const basePayload = { contract_version: SEARCH_PROVIDER_TERRAFORM_CONTRACT_VERSION, deploy_backfill_job: false, environment: context.environment ?? null, name_prefix: providerRuntimeConfig.namePrefix ?? 'atlas-search', network: providerRuntimeConfig.network ?? 'default', platform: providerRuntimeConfig.platform, project_id: context.projectId, provider: context.config.provider, region: resolveSearchCloudRunDeployConfig(context).region, resource_labels: createSearchProviderResourceLabels(context, providerRuntimeConfig.platform), service_account_email: providerRuntimeConfig.serviceAccountEmail ?? resolveSearchCloudRunServiceAccountEmail(context.config, context.projectId), subnetwork: providerRuntimeConfig.subnetwork ?? null }; if (providerRuntimeConfig.platform === 'compute') { basePayload.compute = mapComputeConfig(providerRuntimeConfig.compute); } if (providerRuntimeConfig.platform === 'gke') { basePayload.gke = mapGkeConfig(providerRuntimeConfig.gke); const kubernetes = createSearchProviderTerraformConnectionPayload(options.terraformConnection); if (kubernetes) { basePayload.kubernetes = kubernetes; } } if (context.config.provider === 'typesense') { if (!managedAccess.adminApiKey) { throw new Error('Atlas search provider deploy could not resolve a Typesense admin API key.'); } basePayload.typesense = { admin_api_key: managedAccess.adminApiKey }; return basePayload; } if (!managedAccess.password) { throw new Error('Atlas search provider deploy could not resolve an Elasticsearch admin password.'); } basePayload.elasticsearch = { admin_password: managedAccess.password }; return basePayload; }; export const getSearchProviderTerraformArtifactPath = (projectId, cwd = process.cwd()) => path.join(getAtlasGeneratedTerraformDir(cwd), 'search', `provider.${projectId}.tfvars.json`); export const getSearchProviderPlatformTerraformArtifactPath = (projectId, cwd = process.cwd()) => path.join(getAtlasGeneratedTerraformDir(cwd), 'search', `provider-platform.${projectId}.tfvars.json`); export const writeSearchProviderTerraformArtifact = (context, providerRuntimeConfig, managedAccess, options = {}, cwd = process.cwd()) => { const writeJsonFileImpl = options.writeJsonFile ?? writeJsonFile; const filePath = getSearchProviderTerraformArtifactPath(context.projectId, cwd); const payload = createSearchProviderTerraformPayload(context, providerRuntimeConfig, managedAccess, options); writeJsonFileImpl(filePath, payload); return { contractVersion: payload.contract_version, filePath, payload }; }; export const writeSearchProviderPlatformTerraformArtifact = (context, providerRuntimeConfig, options = {}, cwd = process.cwd()) => { const writeJsonFileImpl = options.writeJsonFile ?? writeJsonFile; const filePath = getSearchProviderPlatformTerraformArtifactPath(context.projectId, cwd); const payload = createSearchProviderPlatformTerraformPayload(context, providerRuntimeConfig); writeJsonFileImpl(filePath, payload); return { contractVersion: payload.contract_version, filePath, payload }; }; const createSearchProviderTerraformWorkflowCommand = (mode, workflowSummary, terraformArtifact) => { const baseArgs = [createTerraformStringVariableArgument(SEARCH_PROVIDER_TERRAFORM_INPUT_VARIABLE, terraformArtifact.filePath), '-input=false']; if (mode === 'plan') { return ['plan', ...baseArgs]; } return [mode, '-auto-approve', ...baseArgs]; }; const createSearchProviderPlatformTerraformWorkflowCommand = (mode, terraformArtifact) => { const baseArgs = [createTerraformStringVariableArgument(SEARCH_PROVIDER_PLATFORM_TERRAFORM_INPUT_VARIABLE, terraformArtifact.filePath), '-input=false']; if (mode === 'plan') { return ['plan', ...baseArgs]; } return [mode, '-auto-approve', ...baseArgs]; }; const createSearchProviderTerraformImportCommand = (terraformArtifact, target) => ['import', createTerraformStringVariableArgument(SEARCH_PROVIDER_TERRAFORM_INPUT_VARIABLE, terraformArtifact.filePath), '-input=false', target.address, target.id]; const createSearchProviderTerraformStateShowCommand = target => ['state', 'show', target.address]; const createSearchProviderPlatformTerraformImportCommand = (terraformArtifact, target) => ['import', createTerraformStringVariableArgument(SEARCH_PROVIDER_PLATFORM_TERRAFORM_INPUT_VARIABLE, terraformArtifact.filePath), '-input=false', target.address, target.id]; const createSearchProviderPlatformTerraformStateShowCommand = target => ['state', 'show', target.address]; const doesSearchProviderImportTargetExist = (target, runGcloudFileCommandImpl = runGcloudFileCommand) => { try { runGcloudFileCommandImpl(target.describeArgs, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }); return true; } catch (error) { if (isGcloudResourceNotFoundError(error)) { return false; } throw new Error(`Could not inspect existing Atlas search provider ${target.kind} ${target.name}. ${getCommandErrorMessage(error)}`); } }; const getSearchProviderTerraformImportTargets = (context, providerRuntimeConfig) => { if (providerRuntimeConfig.platform !== 'compute') { return []; } const runtimeName = createProviderRuntimeName(context, providerRuntimeConfig); const { projectId } = context; const zone = providerRuntimeConfig.compute?.zone; if (!normalizeOptionalString(zone)) { return []; } const providerModuleName = context.config.provider === 'typesense' ? 'typesense_compute' : 'elasticsearch_compute'; const firewallResourceName = context.config.provider === 'typesense' ? 'typesense' : 'elasticsearch'; const targets = [{ address: `module.atlas_search_provider.module.${providerModuleName}[0].google_compute_instance.this`, describeArgs: ['compute', 'instances', 'describe', runtimeName, `--project=${projectId}`, `--zone=${zone}`, '--format="value(name)"'], id: `projects/${projectId}/zones/${zone}/instances/${runtimeName}`, kind: 'instance', name: runtimeName }, { address: `module.atlas_search_provider.module.${providerModuleName}[0].google_compute_disk.data`, describeArgs: ['compute', 'disks', 'describe', `${runtimeName}-data`, `--project=${projectId}`, `--zone=${zone}`, '--format="value(name)"'], id: `projects/${projectId}/zones/${zone}/disks/${runtimeName}-data`, kind: 'disk', name: `${runtimeName}-data` }]; if (providerRuntimeConfig.compute?.createFirewallRule !== false) { targets.push({ address: `module.atlas_search_provider.module.${providerModuleName}[0].google_compute_firewall.${firewallResourceName}[0]`, describeArgs: ['compute', 'firewall-rules', 'describe', `${runtimeName}-${context.config.provider}-ingress`, `--project=${projectId}`, '--format="value(name)"'], id: `projects/${projectId}/global/firewalls/${runtimeName}-${context.config.provider}-ingress`, kind: 'firewall', name: `${runtimeName}-${context.config.provider}-ingress` }); } return targets; }; const getSearchProviderPlatformTerraformImportTargets = (context, providerRuntimeConfig) => { if (!isManagedGkeCluster(providerRuntimeConfig)) { return []; } const clusterConfig = createDefaultGkeClusterConfig(context, providerRuntimeConfig, providerRuntimeConfig.gke?.cluster ?? {}); const nodePoolConfig = createDefaultGkeNodePoolConfig(context.config.provider, providerRuntimeConfig, providerRuntimeConfig.gke?.nodePool ?? {}); return [{ address: 'module.atlas_search_provider_gke_cluster.google_container_cluster.this', describeArgs: ['container', 'clusters', 'describe', clusterConfig.name, `--project=${context.projectId}`, `--location=${clusterConfig.location}`, '--format=value(name)'], id: `projects/${context.projectId}/locations/${clusterConfig.location}/clusters/${clusterConfig.name}`, kind: 'gke-cluster', name: clusterConfig.name }, { address: 'module.atlas_search_provider_gke_node_pool.google_container_node_pool.this', describeArgs: ['container', 'node-pools', 'describe', nodePoolConfig.name, `--project=${context.projectId}`, `--cluster=${clusterConfig.name}`, `--location=${clusterConfig.location}`, '--format=value(name)'], id: `projects/${context.projectId}/locations/${clusterConfig.location}/clusters/${clusterConfig.name}/nodePools/${nodePoolConfig.name}`, kind: 'gke-node-pool', name: nodePoolConfig.name }]; }; export const importExistingSearchProviderTerraformResources = async (context, providerRuntimeConfig, terraformArtifact, workflowSummary, dependencies = {}) => { const runTerraformCommandImpl = dependencies.runTerraformCommand ?? runTerraformCommand; const runGcloudFileCommandImpl = dependencies.runGcloudFileCommand ?? runGcloudFileCommand; const importResults = []; for (const target of getSearchProviderTerraformImportTargets(context, providerRuntimeConfig)) { if (!doesSearchProviderImportTargetExist(target, runGcloudFileCommandImpl)) { importResults.push({ address: target.address, kind: target.kind,