@puls-atlas/cli
Version:
The Puls Atlas CLI tool for managing Atlas projects
1,039 lines • 74.6 kB
JavaScript
import fs from 'fs';
import os from 'os';
import path from 'path';
import inquirer from 'inquirer';
import { execFileSync } from 'child_process';
import { isPlainObject, isString } from 'es-toolkit/compat';
import * as features from '../../utils/feature.js';
import { dedupeMessages, normalizeOptionalString, spreadIf } from '../../utils/value.js';
import { resolveSearchReleaseTarget } from './release.js';
import { parseRequestedSearchIndexes } from './indexScope.js';
import { getSearchProvider, getSearchProviderDisplayName } from './providers/index.js';
import { buildSearchMapperArtifact, executeSearchMapperUpload } from './mapperArtifacts.js';
import { inspectSearchMapperManifest, validateSearchMapperManifest } from './manifestValidation.js';
import { inspectSearchBackfillJob, resolveSearchBackfillJobDeployment } from './backfillJob.js';
import { runSearch, runSearchOperation } from './run.js';
import { prepareSearchRuntimeRegistryAccess } from './runtimeRegistry.js';
import { resolveSearchRuntimeSecretAccessContract } from './runtimeSecrets.js';
import { resolveSearchFirestoreEventarcTriggerRegion } from './firestoreEventarc.js';
import { writeSearchTerraformBackfillJobArtifact } from './terraformAdapter.js';
import { inspectSearchSchemasViaSearchApi, reconcileSearchSchemasViaSearchApi } from './searchApiAdmin.js';
import { SEARCH_API_SERVICE_NAME, SEARCH_SYNC_SERVICE_NAME } from './resourceNames.js';
import { writeSearchRuntimeServicesToProjectConfig } from './config/searchConfig.js';
import { resolveSearchArtifactBucketProvisioning, resolveSearchDlqBucketProvisioning } from './artifactBucket.js';
import { createSearchTerraformWorkflowSummary, getSearchTerraformPrerequisiteTargets, runSearchTerraformWorkflow } from './terraformWorkflow.js';
import { readSearchProviderTerraformOutputs, runSearchProviderDeploy, syncSearchProviderAccessSecret } from './providerDeploy.js';
import { getAtlasGeneratedFeatureConfigPath, logger, formatShellCommand, loadAtlasFeatureCache, runBackgroundOperations, runGcloudFileCommand, writeAtlasFeatureCache, writeAtlasGeneratedFeatureConfig } from '../../utils/index.js';
import { createUnsupportedSearchReindexIndexMessage, getSearchPrivateProviderConnectivityIssues, loadSearchProvisionState, resolveSearchBackfillSupportEntries, resolveSearchCloudRunDeployConfig, resolveSearchProviderSecretReference, resolveSearchSyncFailureInspectionConfig, resolveSearchTaskQueueConfig, resolveSearchRuntimeConfig, resolveSearchRuntimeContract } from './planning.js';
import { createSearchSchemaLifecycleGuidanceEntries, formatSearchSchemaLifecycleStatus, hasSearchSchemaLifecycleState, resolveSearchSchemaLifecycleStatus } from './schemaLifecycle.js';
const getSearchDeployIssues = (context, providerAccess, options = {}) => {
const issues = [];
const cloudRunConfig = resolveSearchCloudRunDeployConfig(context);
const indexNames = Object.keys(context.config.indexes);
const provider = getSearchProvider(context.config.provider);
if (indexNames.length === 0) {
issues.push('Atlas search apply requires at least one configured index in the ' + 'resolved Atlas search config and any referenced search workloads.');
}
if (!cloudRunConfig.region) {
issues.push('Atlas search apply requires "deploy.cloudRun.region" in the active Atlas search config file.');
}
if (!options.dryRun) {
issues.push(...provider.getDeployAccessIssues(providerAccess));
}
issues.push(...getSearchPrivateProviderConnectivityIssues(context, providerAccess));
return issues;
};
const SEARCH_PROVIDER_CONNECTIVITY_RETRY_PATTERN = /\bECONNREFUSED\b|\bECONNRESET\b|\bETIMEDOUT\b|\bEHOSTUNREACH\b|\bENETUNREACH\b|\bENOTFOUND\b|\bEAI_AGAIN\b|getaddrinfo|request timed out|socket hang up/i;
const SEARCH_API_RECONCILIATION_STATUS_RETRY_CODES = new Set([401, 404, 429, 500, 502, 503, 504]);
const DEFAULT_SEARCH_API_RECONCILIATION_RETRY_ATTEMPTS = 4;
const DEFAULT_SEARCH_API_RECONCILIATION_RETRY_DELAY_MS = 5_000;
const SEARCH_SYNC_DLQ_REPLAY_PREFIX = 'search/sync/replay/<collection>/<documentId>/<syncVersion>/';
const SEARCH_RUNTIME_ENV_VARS_FILE_PLACEHOLDER = '[generated-at-execution]';
const SEARCH_PREFLIGHT_UNAVAILABLE_PATTERN = /schema preflight could not be completed before deployment|direct check failed|atlas-search-api check failed/i;
const SEARCH_SCHEMA_DRIFT_PATTERN = /schema drift|does not match the configured schema|schema mismatch|mismatch/i;
const SEARCH_POST_DEPLOY_ACTION_MAP = {
none: {
action: 'none',
follow: false,
requiresBackfillJob: false,
scope: 'all'
},
backfill: {
action: 'backfill',
follow: true,
requiresBackfillJob: true,
scope: 'backfill'
},
'backfill-detached': {
action: 'backfill',
follow: false,
requiresBackfillJob: true,
scope: 'backfill'
},
reindex: {
action: 'reindex',
follow: true,
requiresBackfillJob: true,
scope: 'backfill'
},
'reindex-detached': {
action: 'reindex',
follow: false,
requiresBackfillJob: true,
scope: 'backfill'
},
'reindex-backfill': {
action: 'reindex',
follow: true,
requiresBackfillJob: true,
scope: 'backfill'
},
'reindex-backfill-detached': {
action: 'reindex',
follow: false,
requiresBackfillJob: true,
scope: 'backfill'
},
'reindex-source': {
action: 'reindex',
follow: true,
requiresBackfillJob: false,
scope: 'source-reindex'
}
};
const sleep = timeoutInMilliseconds => new Promise(resolve => {
setTimeout(resolve, timeoutInMilliseconds);
});
const isDirectProviderPreflightUnavailableError = error => SEARCH_PROVIDER_CONNECTIVITY_RETRY_PATTERN.test(isString(error?.message) ? error.message : '');
const isRetryableSearchApiReconciliationError = error => {
const status = Number.isInteger(error?.status) ? error.status : null;
return status !== null && SEARCH_API_RECONCILIATION_STATUS_RETRY_CODES.has(status) || SEARCH_PROVIDER_CONNECTIVITY_RETRY_PATTERN.test(isString(error?.message) ? error.message : '');
};
export const runSearchApiReconciliationWithRetry = async (runReconciliation, {
loggerImpl = logger,
maxAttempts = DEFAULT_SEARCH_API_RECONCILIATION_RETRY_ATTEMPTS,
sleepImpl = sleep,
retryDelayMs = DEFAULT_SEARCH_API_RECONCILIATION_RETRY_DELAY_MS
} = {}) => {
let attempt = 0;
while (attempt < maxAttempts) {
attempt += 1;
try {
return await runReconciliation();
} catch (error) {
const hasRemainingAttempts = attempt < maxAttempts;
if (!hasRemainingAttempts || !isRetryableSearchApiReconciliationError(error)) {
throw error;
}
const waitMs = retryDelayMs * attempt;
loggerImpl.warning(`atlas-search-api schema reconciliation returned a transient response after Cloud Run deployment: ${error.message} ` + `(retry ${attempt}/${maxAttempts - 1} in ${Math.round(waitMs / 1000)}s).`);
await sleepImpl(waitMs);
}
}
throw new Error('Atlas search schema reconciliation retry loop exhausted unexpectedly.');
};
export const runSearchSchemaReconciliationPreflight = async (context, provider) => {
const providerRuntimeConfig = context.config?.deploy?.providerRuntime;
const usesPrivateProviderRuntime = providerRuntimeConfig?.platform === 'compute' && providerRuntimeConfig?.compute?.assignPublicIp !== true;
if (usesPrivateProviderRuntime) {
return {
actions: [],
blockingIssues: [],
warnings: [`Atlas skipped schema preflight because direct ${provider.descriptor.labels.targetGroup.toLowerCase()} access from this machine is unavailable for the configured private provider runtime. ` + 'Atlas will continue deployment and run schema reconciliation after Cloud Run services are deployed.']
};
}
try {
return await provider.reconcileSchemas(context, {
dryRun: true
});
} catch (directError) {
if (isDirectProviderPreflightUnavailableError(directError)) {
return {
actions: [],
blockingIssues: [],
warnings: [`Atlas skipped schema preflight because direct ${provider.descriptor.labels.targetGroup.toLowerCase()} access from this machine is unavailable. ` + 'Atlas will continue deployment and run schema reconciliation after Cloud Run services are deployed.', `Direct preflight reason: ${normalizeOptionalString(directError?.message) ?? String(directError)}`]
};
}
throw new Error('Atlas search schema preflight could not be completed before deployment. ' + `Direct check failed: ${directError.message}`);
}
};
const applyResolvedRuntimeImagesToContext = (context, runtimeRegistryAccess) => {
if (!runtimeRegistryAccess?.images) {
return context;
}
return {
...context,
config: {
...context.config,
deploy: {
...context.config.deploy,
cloudRun: {
...context.config.deploy?.cloudRun,
jobImage: runtimeRegistryAccess.images.job,
serviceImage: runtimeRegistryAccess.images.service,
...spreadIf(runtimeRegistryAccess.images.sourceRunnerJob, {
sourceRunnerJobImage: runtimeRegistryAccess.images.sourceRunnerJob
}),
syncServiceImage: runtimeRegistryAccess.images.syncService
}
}
}
};
};
export const refreshManagedProviderAccessSecret = async (context, managedAccess, options = {}, dependencies = {}, cwd = process.cwd()) => {
const provider = getSearchProvider(context.config.provider);
const providerRuntimeConfig = context.config?.deploy?.providerRuntime;
const providerSecretReference = resolveSearchProviderSecretReference(context);
const secretName = providerSecretReference?.secretName ?? provider.getConfigSecretName(context.config);
if (options.dryRun === true || !providerRuntimeConfig?.platform || !managedAccess) {
return {
endpoint: null,
secretName,
status: 'skipped',
warnings: []
};
}
const readSearchProviderTerraformOutputsImpl = dependencies.readSearchProviderTerraformOutputs ?? readSearchProviderTerraformOutputs;
const syncSearchProviderAccessSecretImpl = dependencies.syncSearchProviderAccessSecret ?? syncSearchProviderAccessSecret;
try {
const terraformOutputs = await readSearchProviderTerraformOutputsImpl(context, providerRuntimeConfig, dependencies, cwd);
return await syncSearchProviderAccessSecretImpl(context, provider, managedAccess, providerRuntimeConfig, terraformOutputs, dependencies);
} catch (error) {
return {
endpoint: null,
secretName,
status: 'unverified',
warnings: [`Could not refresh the managed ${provider.descriptor.displayName} access secret from the local provider Terraform outputs: ${error.message} ` + 'Rerun "atlas search apply" if the provider runtime was recreated or if the provider endpoint changed.']
};
}
};
const createDeployCommand = ({
args,
environmentVariables = null,
kind,
name
}) => ({
name,
args,
environmentVariables,
kind,
executable: 'gcloud',
command: formatShellCommand(['gcloud', ...args])
});
const createRuntimeEnvironmentVariableMap = runtimeContract => Object.fromEntries(runtimeContract.requiredEnvironmentVariables.map(environmentVariable => [environmentVariable.name, environmentVariable.value]));
const renderSearchRuntimeEnvironmentVariablesFileContent = environmentVariables => `${Object.entries(environmentVariables).map(([name, value]) => `${name}: ${JSON.stringify(value ?? '')}`).join('\n')}\n`;
const materializeSearchRuntimeEnvironmentVariablesFile = command => {
const tempDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'atlas-search-env-'));
const filePath = path.join(tempDirectory, `${command.name}.yaml`);
fs.writeFileSync(filePath, renderSearchRuntimeEnvironmentVariablesFileContent(command.environmentVariables), 'utf8');
return {
cleanup: () => {
fs.rmSync(tempDirectory, {
force: true,
recursive: true
});
},
filePath
};
};
const getServiceUrl = (serviceName, projectId, region, runCommand = execFileSync) => runGcloudFileCommand(['run', 'services', 'describe', serviceName, `--project=${projectId}`, '--platform=managed', `--region=${region}`, '--format="value(status.url)"'], {
encoding: 'utf8'
}, runCommand).trim();
const resolveSearchFirestoreEventarcOptions = (context, options = {}) => {
const {
loggerImpl = logger,
runCommand = execFileSync
} = options;
const deployment = resolveSearchBackfillJobDeployment(context);
try {
const firestoreEventarc = resolveSearchFirestoreEventarcTriggerRegion(context, {
runCommand
});
if (firestoreEventarc.triggerRegion !== deployment.region) {
loggerImpl.info(`Detected Firestore database ${firestoreEventarc.databaseId} in ${firestoreEventarc.triggerRegion}. ` + `Atlas will create Eventarc triggers in ${firestoreEventarc.triggerRegion} and keep Cloud Run in ${deployment.region}.`);
}
return {
eventarcDatabase: firestoreEventarc.databaseId,
eventarcTriggerRegion: firestoreEventarc.triggerRegion,
warnings: []
};
} catch (error) {
const warning = 'Could not resolve the Firestore database location for Eventarc. ' + 'Atlas will default to the Cloud Run region for Eventarc triggers; ' + `if Terraform reports a Firestore location mismatch, set deploy.eventarc.region in the active Atlas search config file. ${error.message}`;
loggerImpl.warning(warning);
return {
eventarcDatabase: null,
eventarcTriggerRegion: null,
warnings: [warning]
};
}
};
const executeCommand = (command, runCommand, executionOptions) => {
if (command.executable === 'gcloud') {
return runGcloudFileCommand(command.args, executionOptions, runCommand);
}
return runCommand(command.executable, command.args, executionOptions);
};
const createSearchRuntimeConfigUploadOperation = (runtimeConfigArtifact, runtimeConfigUri) => ({
command: `gcloud storage cp ${runtimeConfigArtifact.filePath} ${runtimeConfigUri}`,
localPath: runtimeConfigArtifact.filePath,
remoteUri: runtimeConfigUri
});
export const prepareSearchRuntimeConfigDeployment = (deployPlan, runtimeConfigArtifact) => {
if (!normalizeOptionalString(deployPlan.runtimeConfigUri)) {
throw new Error('Atlas search apply requires a runtime config URI because inline runtime config transport is no longer supported.');
}
if (!deployPlan.runtimeConfigUri.startsWith('gs://')) {
return {
status: 'external-uri',
uploadOperation: null,
runtimeConfigUri: deployPlan.runtimeConfigUri
};
}
return {
status: 'managed-upload',
runtimeConfigUri: deployPlan.runtimeConfigUri,
uploadOperation: createSearchRuntimeConfigUploadOperation(runtimeConfigArtifact, deployPlan.runtimeConfigUri)
};
};
export const executeSearchRuntimeConfigDeployment = (runtimeConfigDeployment, options = {}) => {
if (!runtimeConfigDeployment || options.dryRun || !runtimeConfigDeployment.uploadOperation) {
return runtimeConfigDeployment;
}
const runCommand = options.runCommand ?? execFileSync;
runGcloudFileCommand(['storage', 'cp', runtimeConfigDeployment.uploadOperation.localPath, runtimeConfigDeployment.uploadOperation.remoteUri], {
stdio: 'inherit'
}, runCommand);
return runtimeConfigDeployment;
};
export const publishFinalizedSearchRuntimeConfig = (deployPlan, runtimeConfigArtifact, options = {}) => executeSearchRuntimeConfigDeployment(prepareSearchRuntimeConfigDeployment(deployPlan, runtimeConfigArtifact), options);
const createSearchUploadOperations = (mapperArtifact, runtimeConfigDeployment, options = {}) => {
const mapperDeploymentOptions = {
dryRun: false,
uploadArtifact: options.uploadMapperArtifact ?? executeSearchMapperUpload
};
const runtimeConfigDeploymentOptions = {
dryRun: false,
runCommand: options.runCommand
};
const operations = [{
name: 'Mapper artifact upload',
run: async () => executeSearchMapperArtifactDeployment(mapperArtifact, mapperDeploymentOptions)
}];
if (runtimeConfigDeployment?.uploadOperation) {
operations.push({
name: 'Runtime config upload',
run: async () => executeSearchRuntimeConfigDeployment(runtimeConfigDeployment, runtimeConfigDeploymentOptions)
});
}
return operations;
};
export const executeSearchUploadOperations = async (mapperArtifact, runtimeConfigDeployment, options = {}) => {
if (options.dryRun === true) {
return {
failedOutcomes: [],
outcomes: []
};
}
const runBackgroundOperationsImpl = options.runBackgroundOperations ?? runBackgroundOperations;
const outcomes = await runBackgroundOperationsImpl(createSearchUploadOperations(mapperArtifact, runtimeConfigDeployment, options), {
loggerImpl: options.loggerImpl ?? logger,
runInBackground: options.runInBackground !== false
});
return {
failedOutcomes: outcomes.filter(outcome => outcome.status !== 'completed'),
outcomes
};
};
const formatUploadOutcomeRows = uploadSummary => uploadSummary.outcomes.map(outcome => ({
label: outcome.name,
tone: outcome.status === 'completed' ? 'success' : 'error',
value: outcome.status === 'completed' ? 'completed' : `failed: ${outcome.error?.message ?? 'Unexpected upload failure.'}`
}));
const createUploadFailureMessages = uploadSummary => uploadSummary.failedOutcomes.map(outcome => `${outcome.name} failed while publishing Atlas search artifacts. ${outcome.error?.message ?? 'Unexpected upload failure.'}`);
const createUnavailableSourceReindexWarning = (indexName, schemaEntry = null) => {
const reconciliationStatus = normalizeOptionalString(schemaEntry?.reconciliationStatus) ?? 'unknown';
if (reconciliationStatus === 'bootstrap-required') {
return `Atlas search source-backed reindex is not available for ${indexName} yet because the live target still needs bootstrap. ` + 'Rerun "atlas search apply" first so Atlas can create the initial target, then retry reindex.';
}
return `Atlas search source-backed reindex is not available for ${indexName} right now ` + `(schema lifecycle: ${reconciliationStatus}). Resolve the reported Atlas search schema state first, then retry reindex.`;
};
const createSearchPostDeployChoices = ({
backfillIndexNames,
sourceReindexIndexNames
}) => {
const choices = [{
name: 'Do nothing',
value: 'none'
}];
const hasSourceReindexChoices = sourceReindexIndexNames.length > 0;
const backfillScopeLabel = backfillIndexNames.join(', ');
const sourceReindexScopeLabel = sourceReindexIndexNames.join(', ');
if (backfillIndexNames.length > 0) {
choices.push({
name: hasSourceReindexChoices ? `Start a backfill job and follow until completion for Firestore-backed indexes: ${backfillScopeLabel}` : 'Start a backfill job and follow until completion',
value: 'backfill'
}, {
name: hasSourceReindexChoices ? `Start a backfill job in detached mode for Firestore-backed indexes: ${backfillScopeLabel}` : 'Start a backfill job in detached mode',
value: 'backfill-detached'
}, {
name: hasSourceReindexChoices ? `Start a reindex job and follow until completion for Firestore-backed indexes: ${backfillScopeLabel}` : 'Start a reindex job and follow until completion',
value: hasSourceReindexChoices ? 'reindex-backfill' : 'reindex'
}, {
name: hasSourceReindexChoices ? `Start a reindex job in detached mode for Firestore-backed indexes: ${backfillScopeLabel}` : 'Start a reindex job in detached mode',
value: hasSourceReindexChoices ? 'reindex-backfill-detached' : 'reindex-detached'
});
}
if (sourceReindexIndexNames.length > 0) {
choices.push({
name: `Start a source-backed reindex and follow until completion for indexes: ${sourceReindexScopeLabel}`,
value: 'reindex-source'
});
}
return choices;
};
const resolveSearchPostDeployActionIndexNames = (selectedAction, {
backfillIndexNames,
sourceReindexIndexNames
}) => {
if (selectedAction?.action === 'none') {
return [];
}
if (selectedAction?.scope === 'source-reindex') {
return sourceReindexIndexNames;
}
if (selectedAction?.scope === 'backfill') {
return backfillIndexNames;
}
return [...backfillIndexNames, ...sourceReindexIndexNames];
};
const resolveSearchPostDeployActionSelection = selectedAction => {
const normalizedAction = normalizeOptionalString(selectedAction)?.toLowerCase();
return normalizedAction ? SEARCH_POST_DEPLOY_ACTION_MAP[normalizedAction] ?? null : null;
};
const normalizeDeployInvocation = (indexesArgument, options = {}) => {
if (isPlainObject(indexesArgument)) {
return {
indexesArgument: undefined,
options: indexesArgument
};
}
return {
indexesArgument,
options: options ?? {}
};
};
export const resolveSearchApplyPinnedProjectOptions = (options = {}, context = null) => {
const pinnedProjectId = normalizeOptionalString(context?.projectId);
if (!pinnedProjectId) {
return {
...(options ?? {})
};
}
return {
...(options ?? {}),
project: pinnedProjectId
};
};
const assertSearchDeployDoesNotAcceptIndexScope = indexesArgument => {
if (!normalizeOptionalString(indexesArgument)) {
return;
}
throw new Error('Atlas search apply always applies the full consumer-project stack and does not accept an index scope. ' + 'Use "atlas search run <backfill|reindex> --indexes <...>" for scoped runtime jobs.');
};
const hasConfiguredProviderRuntimePlatform = context => {
const providerRuntimeConfig = context?.config?.deploy?.providerRuntime;
return isPlainObject(providerRuntimeConfig) && Boolean(normalizeOptionalString(providerRuntimeConfig.platform));
};
const shouldDeployProviderRuntime = (context, options = {}) => {
if (hasConfiguredProviderRuntimePlatform(context)) {
return true;
}
return !!(normalizeOptionalString(options.platform) || normalizeOptionalString(options.zone));
};
export const formatSearchDeployError = (error, phase, context = null) => {
const normalizedPhase = normalizeOptionalString(phase) ?? 'running Atlas search apply';
const errorMessage = normalizeOptionalString(error?.message) ?? '';
const hasQueueCooldownError = /queue cannot be created because a queue with this name existed too recently/i.test(errorMessage) || /Error creating Queue/i.test(errorMessage);
const messageParts = [`Atlas search apply failed while ${normalizedPhase}${context?.projectId ? ` for project ${context.projectId}` : ''}.`, errorMessage];
if (/request failed|request timed out|Could not inspect Typesense|Could not inspect Elasticsearch|fetch failed/i.test(errorMessage)) {
messageParts.push('Check the configured search provider endpoint and credentials, verify that atlas-search-api can reach the provider over the network, and retry "atlas search apply". If the provider runtime already exists, "atlas search verify" can help confirm the remaining state once connectivity is restored.');
}
if (normalizedPhase.startsWith('preflighting ')) {
const preflightUnavailable = SEARCH_PREFLIGHT_UNAVAILABLE_PATTERN.test(errorMessage);
const schemaDriftDetected = SEARCH_SCHEMA_DRIFT_PATTERN.test(errorMessage);
if (preflightUnavailable && !schemaDriftDetected) {
messageParts.push('Atlas could not complete schema preflight before deployment. If this is a first-time deployment and atlas-search-api is not created yet, rerun "atlas search apply" to provision runtime services first. ' + 'Otherwise restore provider connectivity and rerun "atlas search apply". Use "atlas search verify" once connectivity is restored to inspect any remaining drift.');
} else {
messageParts.push('Resolve the reported schema drift first, then rerun "atlas search apply". ' + 'Use "atlas search verify" for a full drift report and run "atlas search run reindex --indexes <...>" when the schema change is intentional.');
}
} else if (normalizedPhase === 'resolving provider access' || normalizedPhase.startsWith('reconciling ')) {
messageParts.push('If Atlas is responsible for the provider runtime, rerun "atlas search apply" so the provider endpoint and secret are refreshed.');
} else if (normalizedPhase === 'loading the search provision state' || normalizedPhase === 'verifying the uploaded mapper manifest') {
messageParts.push('Run "atlas search apply --dry-run" after fixing the underlying issue so Atlas can refresh provider access, queue, and manifest readiness before retrying "atlas search apply".');
} else if (normalizedPhase === 'running the Terraform workflow' || normalizedPhase === 'deploying Cloud Run services' || normalizedPhase === 'finalizing generated runtime configuration') {
messageParts.push('Once the deployment issue is fixed, rerun "atlas search apply" and then use "atlas search verify" to confirm the final search state.');
}
if (hasQueueCooldownError) {
messageParts.push('Cloud Tasks rejected queue creation because the queue name is still in cooldown after a recent delete/recreate. ' + 'Avoid deleting the existing search sync queue and rerun "atlas search apply" once the cooldown window expires. ' + 'If needed, set a temporary queue name in your search task queue config and retry apply.');
}
return messageParts.join(' ');
};
export { parseRequestedSearchIndexes };
const createSearchPostDeployDependencies = (context, dependencies, {
backfillJobInspection = null,
now,
runCommand
}) => ({
...dependencies,
...(backfillJobInspection ? {
ensureSearchBackfillJobExists: () => backfillJobInspection
} : {}),
loadFeatureContext: async () => context,
now,
runCommand
});
export const executeSearchPostDeployAction = async (context, indexNames, options = {}, dependencies = {}) => {
if (options.dryRun) {
return {
indexNames,
status: 'dry-run'
};
}
const prompt = dependencies.prompt ?? inquirer.prompt;
const inspectSearchBackfillJobImpl = dependencies.inspectSearchBackfillJob ?? inspectSearchBackfillJob;
const inspectSearchSchemasViaSearchApiImpl = dependencies.inspectSearchSchemasViaSearchApi ?? inspectSearchSchemasViaSearchApi;
const runSearchImpl = dependencies.runSearch ?? runSearch;
const runSearchOperationImpl = dependencies.runSearchOperation ?? runSearchOperation;
const runCommand = dependencies.runCommand ?? execFileSync;
const now = dependencies.now ?? (() => new Date());
const backfillSupportEntries = resolveSearchBackfillSupportEntries(context, indexNames);
const unsupportedIndexEntries = backfillSupportEntries.filter(indexEntry => !indexEntry.supportsBackfill && !indexEntry.supportsSourceReindex && indexEntry.enabledSourceNames.length > 0);
const backfillRunnableIndexNames = backfillSupportEntries.filter(indexEntry => indexEntry.supportsBackfill).map(indexEntry => indexEntry.indexName);
let sourceReindexRunnableIndexNames = backfillSupportEntries.filter(indexEntry => !indexEntry.supportsBackfill && indexEntry.supportsSourceReindex).map(indexEntry => indexEntry.indexName);
const blockedSourceReindexIndexNames = [];
if (sourceReindexRunnableIndexNames.length > 0) {
try {
const schemaInspection = await inspectSearchSchemasViaSearchApiImpl(context, dependencies);
const schemaEntries = Array.isArray(schemaInspection?.collections) ? schemaInspection.collections : [];
const schemaEntryByIndexName = new Map(schemaEntries.map(schemaEntry => [schemaEntry.indexName, schemaEntry]));
sourceReindexRunnableIndexNames = sourceReindexRunnableIndexNames.filter(indexName => {
const schemaEntry = schemaEntryByIndexName.get(indexName) ?? null;
const reconciliationStatus = normalizeOptionalString(schemaEntry?.reconciliationStatus);
if (reconciliationStatus === 'match' || reconciliationStatus === 'reindex-required' || reconciliationStatus === 'candidate-active' || reconciliationStatus === 'promotable' || reconciliationStatus === 'cleanup-pending') {
return true;
}
blockedSourceReindexIndexNames.push(indexName);
logger.warning(createUnavailableSourceReindexWarning(indexName, schemaEntry));
return false;
});
} catch (error) {
blockedSourceReindexIndexNames.push(...sourceReindexRunnableIndexNames);
sourceReindexRunnableIndexNames = [];
logger.warning('Atlas could not inspect schema lifecycle for source-backed reindex follow-up choices. ' + `${error.message} Atlas will skip those follow-up choices until the runtime state can be inspected again.`);
}
}
const runnableIndexNames = [...backfillRunnableIndexNames, ...sourceReindexRunnableIndexNames];
for (const indexEntry of unsupportedIndexEntries) {
logger.warning(createUnsupportedSearchReindexIndexMessage(indexEntry));
}
if (runnableIndexNames.length === 0) {
return {
indexNames,
status: 'skipped',
unsupportedIndexNames: [...unsupportedIndexEntries.map(indexEntry => indexEntry.indexName), ...blockedSourceReindexIndexNames]
};
}
const scopeLabel = runnableIndexNames.join(', ');
const backfillActionsOnly = backfillRunnableIndexNames.length > 0 && sourceReindexRunnableIndexNames.length === 0;
let backfillJobInspection = null;
if (backfillActionsOnly) {
backfillJobInspection = inspectSearchBackfillJobImpl(context, {
runCommand
});
if (!backfillJobInspection.exists) {
logger.warning(`Terraform-managed backfill job ${backfillJobInspection.jobName} was not found in ${backfillJobInspection.projectId}/${backfillJobInspection.region}. ` + 'Atlas search apply normally creates this job automatically. Verify search.deploy.terraform.rootDir and rerun atlas search apply.');
return {
indexNames,
jobName: backfillJobInspection.jobName,
status: 'blocked'
};
}
}
const {
action: selectedAction
} = await prompt([{
choices: createSearchPostDeployChoices({
backfillIndexNames: backfillRunnableIndexNames,
sourceReindexIndexNames: sourceReindexRunnableIndexNames
}),
default: 'none',
message: `Deployment completed. What should Atlas search do next for indexes: ${scopeLabel}?`,
name: 'action',
type: 'select'
}]);
const selectedPostDeployAction = resolveSearchPostDeployActionSelection(selectedAction);
if (!selectedPostDeployAction) {
throw new Error(`Unsupported Atlas search post-deploy action: ${selectedAction ?? '(empty)'}.`);
}
const selectedIndexNames = resolveSearchPostDeployActionIndexNames(selectedPostDeployAction, {
backfillIndexNames: backfillRunnableIndexNames,
sourceReindexIndexNames: sourceReindexRunnableIndexNames
});
if (selectedPostDeployAction.action === 'none') {
logger.info('No Atlas search post-deploy action was selected.');
return {
action: selectedPostDeployAction.action,
indexNames: selectedIndexNames,
status: 'skipped'
};
}
if (selectedPostDeployAction.requiresBackfillJob && !backfillJobInspection) {
backfillJobInspection = inspectSearchBackfillJobImpl(context, {
runCommand
});
if (!backfillJobInspection.exists) {
logger.warning(`Terraform-managed backfill job ${backfillJobInspection.jobName} was not found in ${backfillJobInspection.projectId}/${backfillJobInspection.region}. ` + 'Atlas search apply normally creates this job automatically. Verify search.deploy.terraform.rootDir and rerun atlas search apply.');
return {
indexNames,
jobName: backfillJobInspection.jobName,
status: 'blocked'
};
}
}
const postDeployDependencies = createSearchPostDeployDependencies(context, dependencies, {
backfillJobInspection,
now,
runCommand
});
const joinedIndexNames = selectedIndexNames.join(',');
if (!selectedPostDeployAction.follow) {
return runSearchOperationImpl(selectedPostDeployAction.action, joinedIndexNames, {}, postDeployDependencies);
}
const followResult = await runSearchImpl({
action: selectedPostDeployAction.action,
indexes: joinedIndexNames,
timeout: options.timeout
}, {
...postDeployDependencies,
exit: code => code
}, process.cwd());
if (followResult === 1) {
return {
action: selectedPostDeployAction.action,
follow: true,
indexNames: selectedIndexNames,
status: 'failed'
};
}
return followResult;
};
const formatEnvironmentVariableValueForLog = environmentVariable => {
const value = environmentVariable.value ?? '';
if (isString(value) && value.length > 160) {
return `[value omitted, ${Buffer.byteLength(value, 'utf8')} bytes]`;
}
return value;
};
const createRuntimeSecretArgs = (runtimeContract, serviceName) => runtimeContract.requiredSecretEnvironmentVariables.filter(secretReference => secretReference.services?.includes(serviceName)).map(secretReference => `${secretReference.env}=${secretReference.secretName}:${secretReference.version}`);
const createCloudRunServiceDeployCommand = ({
cloudRunConfig,
environmentVariables,
projectId,
secretArgs,
serviceName,
unauthenticated,
vpcAccessArgs
}) => createDeployCommand({
args: ['run', 'deploy', serviceName, `--project=${projectId}`, '--platform=managed', `--region=${cloudRunConfig.region}`, `--image=${serviceName === SEARCH_SYNC_SERVICE_NAME ? cloudRunConfig.syncServiceImage : cloudRunConfig.serviceImage}`, `--service-account=${cloudRunConfig.serviceAccountEmail}`, `--env-vars-file=${SEARCH_RUNTIME_ENV_VARS_FILE_PLACEHOLDER}`, ...(secretArgs.length > 0 ? [`--set-secrets=${secretArgs.join(',')}`] : []), ...vpcAccessArgs, unauthenticated ? '--allow-unauthenticated' : '--no-allow-unauthenticated'],
environmentVariables,
kind: 'service',
name: serviceName
});
export const createSearchDeployPlan = (context, options = {}) => {
const cloudRunConfig = resolveSearchCloudRunDeployConfig(context);
const backfillJobDeployment = resolveSearchBackfillJobDeployment(context);
const provider = getSearchProvider(context.config.provider);
const runtimeContract = resolveSearchRuntimeContract(context);
const providerSecretReference = resolveSearchProviderSecretReference(context);
const syncFailureInspection = resolveSearchSyncFailureInspectionConfig(context);
const taskQueue = resolveSearchTaskQueueConfig(context);
const engineRuntimeContract = provider.getEngineRuntimeContract ? provider.getEngineRuntimeContract(context, {
cloudRunConfig
}) : null;
const commonEnvironmentVariables = createRuntimeEnvironmentVariableMap(runtimeContract);
const syncSecretArgs = createRuntimeSecretArgs(runtimeContract, SEARCH_SYNC_SERVICE_NAME);
const apiSecretArgs = createRuntimeSecretArgs(runtimeContract, SEARCH_API_SERVICE_NAME);
const images = {
job: cloudRunConfig.jobImage,
service: cloudRunConfig.serviceImage,
...(cloudRunConfig.sourceRunnerJobImage ? {
sourceRunnerJob: cloudRunConfig.sourceRunnerJobImage
} : {}),
syncService: cloudRunConfig.syncServiceImage
};
const vpcAccessArgs = cloudRunConfig.vpcAccess ? [`--network=${cloudRunConfig.vpcAccess.network}`, ...(cloudRunConfig.vpcAccess.subnetwork ? [`--subnet=${cloudRunConfig.vpcAccess.subnetwork}`] : []), `--vpc-egress=${cloudRunConfig.vpcAccess.egress}`] : [];
const commands = [createCloudRunServiceDeployCommand({
cloudRunConfig,
environmentVariables: commonEnvironmentVariables,
projectId: context.projectId,
secretArgs: syncSecretArgs,
serviceName: SEARCH_SYNC_SERVICE_NAME,
unauthenticated: false,
vpcAccessArgs
}), createDeployCommand({
args: ['run', 'services', 'add-iam-policy-binding', SEARCH_SYNC_SERVICE_NAME, `--project=${context.projectId}`, '--platform=managed', `--region=${cloudRunConfig.region}`, `--member=serviceAccount:${context.projectId}@appspot.gserviceaccount.com`, '--role=roles/run.invoker'],
kind: 'policy',
name: `${SEARCH_SYNC_SERVICE_NAME}-invoker`
}), createCloudRunServiceDeployCommand({
cloudRunConfig,
environmentVariables: commonEnvironmentVariables,
projectId: context.projectId,
secretArgs: apiSecretArgs,
serviceName: SEARCH_API_SERVICE_NAME,
unauthenticated: true,
vpcAccessArgs
})];
return {
backfillJobDeployment,
commands,
createdAt: new Date().toISOString(),
environment: context.environment,
images,
mapperManifestUri: cloudRunConfig.mapperManifestUri,
runtimeConfigUri: cloudRunConfig.runtimeConfigUri,
searchConfigTransport: cloudRunConfig.searchConfigTransport,
engineRuntimeContract,
projectId: context.projectId,
release: context.config.release ?? null,
releaseTarget: resolveSearchReleaseTarget(context.config?.release),
region: cloudRunConfig.region,
syncFailureInspection,
taskQueue,
runtimeRegistry: {
location: cloudRunConfig.artifactRegistryLocation,
project: cloudRunConfig.artifactRegistryProject,
repository: cloudRunConfig.repository,
tag: cloudRunConfig.tag
},
runtimeRegistryAccess: options.runtimeRegistryAccess ?? null,
runtimeSecretAccess: options.runtimeSecretAccess ?? null,
serviceAccountEmail: cloudRunConfig.serviceAccountEmail,
vpcAccess: cloudRunConfig.vpcAccess,
terraform: createSearchTerraformWorkflowSummary(context.config),
runtimeContract,
secrets: {
apiRequestAuth: runtimeContract.requiredSecretEnvironmentVariables[0] ?? null,
providerConfig: providerSecretReference
},
version: 1
};
};
const logDeployPlan = deployPlan => {
logger.summary('Deploy summary', [{
label: 'Project',
value: deployPlan.projectId
}, deployPlan.environment ? {
label: 'Environment',
value: deployPlan.environment
} : null, {
label: 'Region',
value: deployPlan.region
}, {
label: 'Release strategy',
value: `${deployPlan.release?.strategy ?? 'direct'}${deployPlan.release?.active ? ` [active=${deployPlan.release.active}]` : ''}${deployPlan.release?.candidate ? ` [candidate=${deployPlan.release.candidate}]` : ''}`
}, {
label: 'Release target',
value: `${deployPlan.releaseTarget?.target ?? 'direct'}${deployPlan.releaseTarget?.releaseId ? ` [id=${deployPlan.releaseTarget.releaseId}]` : ''}`
}, {
label: 'Runtime registry',
value: `${deployPlan.runtimeRegistry.location}-docker.pkg.dev/${deployPlan.runtimeRegistry.project}/${deployPlan.runtimeRegistry.repository}`
}, {
label: 'Runtime image tag',
value: deployPlan.runtimeRegistry.tag
}, {
label: 'Mapper manifest URI',
value: deployPlan.mapperManifestUri ?? 'not configured'
}, {
label: 'Search config transport',
value: deployPlan.searchConfigTransport
}, {
label: 'Runtime config URI',
value: deployPlan.runtimeConfigUri ?? 'not configured'
}, {
label: 'Cloud Run VPC access',
value: deployPlan.vpcAccess ? `${deployPlan.vpcAccess.network}${deployPlan.vpcAccess.subnetwork ? `/${deployPlan.vpcAccess.subnetwork}` : ''} [egress=${deployPlan.vpcAccess.egress}]` : 'disabled'
}, {
label: 'Search sync task queue',
value: `${deployPlan.taskQueue.name} [${deployPlan.taskQueue.location}]`
}, {
label: 'Sync failure state collection',
value: deployPlan.syncFailureInspection.collectionPath ?? 'not configured for CLI inspection'
}, {
label: 'Runtime env vars',
value: `${deployPlan.runtimeContract.requiredEnvironmentVariables.length} required`
}, deployPlan.runtimeContract.requiredSecretEnvironmentVariables.length > 0 ? {
label: 'Runtime secret env vars',
value: `${deployPlan.runtimeContract.requiredSecretEnvironmentVariables.length} required`
} : null, {
label: 'Provider secret reference',
value: deployPlan.secrets.providerConfig.secretName
}, {
label: 'API request auth secret',
value: deployPlan.secrets.apiRequestAuth?.secretName ?? 'not configured'
}, deployPlan.runtimeRegistryAccess?.serviceAgentEmail ? {
label: 'Cloud Run service agent',
value: deployPlan.runtimeRegistryAccess.serviceAgentEmail
} : null, deployPlan.serviceAccountEmail ? {
label: 'Cloud Run runtime service account',
value: deployPlan.serviceAccountEmail
} : null, {
label: 'Terraform-managed runtime resources',
value: deployPlan.terraform.managedResources.length
}, {
label: 'Planned deploy commands',
value: deployPlan.commands.length
}, deployPlan.engineRuntimeContract ? {
label: 'Provider runtime contract',
value: `${deployPlan.engineRuntimeContract.requiredEnvironmentVariables.length} env vars, ${deployPlan.engineRuntimeContract.requiredSecretFields.length} secret fields, ${deployPlan.engineRuntimeContract.manualDeploymentInstructions.length} manual notes`
} : null]);
logger.section('Runtime images', [`Service image: ${deployPlan.images.service}`, `Sync service image: ${deployPlan.images.syncService}`, `Job image: ${deployPlan.images.job}`, deployPlan.images.sourceRunnerJob ? `Source runner job image: ${deployPlan.images.sourceRunnerJob}` : null].filter(Boolean), {
detailOnly: true
});
logger.section('Runtime environment', deployPlan.runtimeContract.requiredEnvironmentVariables.map(environmentVariable => `${environmentVariable.name}=${formatEnvironmentVariableValueForLog(environmentVariable)} [required, injected by ${environmentVariable.source}]`), {
detailOnly: true
});
if (deployPlan.runtimeContract.requiredSecretEnvironmentVariables.length > 0) {
logger.section('Runtime secret environment', deployPlan.runtimeContract.requiredSecretEnvironmentVariables.map(secretEnvironmentVariable => `${secretEnvironmentVariable.env} -> ${secretEnvironmentVariable.secretName}:${secretEnvironmentVariable.version} [required, injected by ${secretEnvironmentVariable.source}]`), {
detailOnly: true
});
}
if (deployPlan.runtimeContract.localDevelopmentFallbackEnvVars.length > 0) {
logger.section('Local development fallbacks', [deployPlan.runtimeContract.localDevelopmentFallbackEnvVars.join(', ')], {
detailOnly: true
});
}
if ((deployPlan.runtimeRegistryAccess?.repositoryAccess?.length ?? 0) > 0) {
logger.section('Shared Artifact Registry access', deployPlan.runtimeRegistryAccess.repositoryAccess.map(access => `${access.repositoryUri}: ${access.status}`), {
detailOnly: true
});
}
if ((deployPlan.runtimeSecretAccess?.secretAccess?.length ?? 0) > 0) {
logger.section('Runtime secret access', deployPlan.runtimeSecretAccess.secretAccess.map(access => `${access.secretResource}: ${access.status}`), {
detailOnly: true
});
}
if (deployPlan.engineRuntimeContract) {
logger.summary('Provider runtime contract', [{
label: 'Responsibility',
value: deployPlan.engineRuntimeContract.responsibility
}, {
label: 'Secret',
value: deployPlan.engineRuntimeContract.providerSecret
}], {
detailOnly: true
});
logger.section('Provider runtime environment', deployPlan.engineRuntimeContract.requiredEnvironmentVariables.map(environmentVariable => environmentVariable.source ? `${environmentVariable.name} [${environmentVariable.source}]` : `${environmentVariable.name}=${environmentVariable.value}`), {
detailOnly: true
});
logger.section('Provider secret payload', deployPlan.engineRuntimeContract.requiredSecretFields.map(secretField => `${secretField.name}: ${secretField.description}`), {
detailOnly: true
});
logger.section('Provider manual deployment notes', deployPlan.engineRuntimeContract.manualDeploymentInstructions, {
detailOnly: true
});
}
logger.summary('Terraform workflow', [{
label: 'Terraform root',
value: deployPlan.terraform.rootPath
}, {
label: 'Terraform module source',
value: deployPlan.terraform.moduleSource
}, ...deployPlan.terraform.managedResources.map(resource => ({
label: `${resource.kind} ${resource.name}`,
value: resource.description
}))], {
detailOnly: true
});
logger.section('Planned deploy commands', deployPlan.commands.map(command => `${command.kind} ${command.name}: ${command.command}`), {
detailOnly: true
});
};
const logWarnings = warnings => {
for (const warning of dedupeMessages(warnings)) {
logger.warning(warning);
}
};
const collectSearchDeployWarnings = (...warningGroups) => dedupeMessages(warningGroups.flat());
const logSearchDeployArtifactPaths = ({
cacheFilePath = null,
context,
detailOnly = true,
loggerImpl = logger,
runtimeConfigArtifact,
terraformArtifact,
terraformRoot
}) => {
loggerImpl.summary('Local artifacts', [{
label: 'Source config',
value: context.configPath
}, {
label: 'Runtime config',
value: runtimeConfigArtifact.filePath
}, cacheFilePath ? {
label: 'Cache file',
value: cacheFilePath
} : null, {
label: 'Terraform search adapter',
value: terraformArtifact.filePath
}, {
label: 'Terraform root',
value: terraformRoot
}], {
detailOnly
});
};
const reportSearchDeployBlockingIssues = ({
cacheFilePath = null,
context,
issues,
runtimeConfigArtifact,
spinner,
spinnerMessage,
terraformArtifact,
terraformRoot,
warnings = []
}) => {
spinner.fail(spinnerMessage);
logSearchDeployArtifactPaths({
cacheFilePath,
context,
detailOnly: false,
runtimeConfigArtifact,
terraformArtifact,
terraformRoot
});
for (const issue of issues) {
logger.error(issue, false);
}
logWarnings(warnings);
};
const logProviderTargetActions = (context, provider, actions) => {
if (actions.length === 0) {
return;
}
logger.summary(`${provider.descriptor.labels.targetGroup} reconciliation`, actions.map(action => {
const actionLabel = hasSearchSchemaLifecycleState(action) ? formatSearchSchemaLifecycleStatus(resolveSearchSchemaLifecycleStatus(action)) : normalizeOptionalString(action?.action)?.replace(/-/g, ' ') ?? 'unknown';
return `${action.name}: ${actionLabel}`;
}));
const guidanceEntries = createSearchSchemaLifecycleGuidanceEntries(actions, {
context
});
if (guidanceEntries.length > 0) {
logger.summary('Operator guidance', guidanceEntries);
}
};
const logMapperArtifactOperations = artifact => {
logger.summary('Mapper artifacts', [{
label: 'Mapper manifest URI',
value: artifact.destinationUri
}, {
label: 'Upload operations',
value: artifact.uploadOperations.length
}]);
logger.summary('Mapper artifact files', [{
label: 'Mapper artifact directory',
value: artifact.artifactRoot
}, {
label: 'Mapper manifest file',
value: artifact.manifestFilePath
}], {
detailOnly: true
});
logger.section('Mapper upload commands', artifact.uploadOperations.map(operation => `gcloud storage cp ${operation.localPath} ${operation.remoteUri}`), {
detailOnly: true
});
};
const logRuntimeConfigDeployment = runtimeConfigDeployment => {
if (!runtimeConfigDeployment) {
return;
}
logger.summary('Runtime config', [{
label: 'Transport',
value: runtimeConfigDeployment.status === 'managed-upload' ? 'artifact upload' : 'artifact uri (external)'
}, {
label: 'Runtime config URI',
value: runtimeConfigDeployment.runtimeConfigUri
}]);
if (runtimeConfigDeployment.uploadOperation) {
logger.section('Runtime config upload command', [runtimeConfigDeployment.uploadOperation.command], {
detailOnly: true
});
}
};
const formatArtifactBucketStatusRow = (artifactBucket, {
label = 'Mapper artifact bucket'
} = {}) => {
if (!artifactBucket || artifactBucket.status === 'skipped') {
return null;
}
if (artifactBucket.status === 'existing') {
return {
label,
tone: 'success',
value: formatArtifactBucketSummaryValue(artifactBucket, 'existing')
};
}
if (artifactBucket.status === 'created') {
return {
label,
tone: 'success',
value: formatArtifactBucketSummaryValue(artifactBucket, 'created')
};
}
if (artifactBucket.status === 'would-create') {
return {
label,
tone: 'warning',
value: formatArtifactBucketSummaryValue(artifactBucket, 'dry-run: would create')
};
}
if (artifactBucket.status === 'terraform-managed') {
return {
label,
tone: 'success',
value: formatArtifactBucketSummaryValue(artifactBucket, 'terraform-managed')
};
}
return null;
};
const formatArtifactBucketSummaryValue = (artifactBucket, statusLabel) => {
const retentionSuffix = Number.isInteger(artifactBucket?.retentionDays) ? `; retention=${artifactBucket.retentionDays}d` : '';
return `gs://${artifactBucket.bucketName} [${statusLabel}${retentionSuffix}]`;
};
export const runSearchDeployTerraformPrerequisites = async (context, terraformArtifact, options = {}, dependencies = {}, cwd = process.cwd()) => {
const runSearchTerraformWorkflowImpl = dependencies.runSearchTerraformWorkflow ?? runSearchTerraformWorkflow;
const onTerraformPrerequisitesStartImpl = dependencies.onTerraformPrerequisitesStart ?? (() => {});
const prerequisiteTargets = getSearchTerraformPrerequisiteTargets(terraformArtifact);
if (options.dryRun || prerequisiteTargets.length === 0) {
return null;
}
onTerraformPrerequisitesStartImpl();
return runSearchTerraformWorkflowImpl(context.config, terraformArtifact, {
applyTargetsMode: 'missing',
mode: 'apply',
targets: prerequisiteTargets
}, {
runGcloudFileCommand: dependencies.runGcloudFileCommand,
runTerraformCommand: dependencies.runTerraformCommand
}, cwd);
};
export const executeSearchDeployPlan = async (deployPlan, options = {}) => {
const {
runCommand = execFileSync
} = options;
for (const command of deployPlan.commands) {
logger.info(`Deploying ${command.kind} ${command.name}...`);
const materializedEnvironmentVariables = command.environmentVariables ? materializeSearchRuntimeEnvironmentVariablesFile(command) : null;
const materializedCommand = materializedEnvironmentVariables ? {
...command,
args: command.args.map(argument => argument === `--env-vars-file=${SEARCH_RUNTIME_ENV_VARS_FILE_PLACEHOLDER}` ? `--env-vars-file=${