UNPKG

@puls-atlas/cli

Version:

The Puls Atlas CLI tool for managing Atlas projects

502 lines 25.2 kB
import fs from 'fs'; import path from 'path'; import { resolveRootPath } from '../../utils/atlas.js'; import { writeJsonFile } from '../../utils/file.js'; import { isGcloudResourceNotFoundError, runGcloudFileCommand } from '../../utils/gcloud.js'; import { SEARCH_RESOURCE_NAMES } from './resourceNames.js'; import { getEnabledSearchEventarcCollectionPlans } from './eventarc.js'; import { ATLAS_SEARCH_TASK_QUEUE_NAME } from '../../utils/taskQueue.js'; import { normalizeOptionalString, spreadIf } from '../../utils/value.js'; import { getEnabledScheduledSearchSourcePlans } from './sources/scheduled.js'; import { ensureTerraformWorkspace, createTerraformStringVariableArgument, runTerraformCommand } from '../../utils/terraform.js'; import { SEARCH_TERRAFORM_CONTRACT_VERSION, SEARCH_TERRAFORM_INPUT_VARIABLE } from './terraformAdapter.js'; import { DEFAULT_SEARCH_TERRAFORM_MODULE_SOURCE, DEFAULT_SEARCH_TERRAFORM_ROOT_DIR } from './config/searchConfig.js'; import { loadSearchTerraformRuntimeRootTemplateFiles, renderSearchTerraformRootTemplateFiles } from './terraformRootTemplates.js'; const SEARCH_TERRAFORM_IMPORT_ALREADY_MANAGED_PATTERN = /already managed by Terraform/iu; const SEARCH_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 GCLOUD_PERMISSION_DENIED_ERROR_PATTERN = /(permission[ -]?denied|PERMISSION_DENIED|does not have permission|insufficient permission|not authorized|forbidden|iam\.serviceAccounts\.get)/iu; const SEARCH_TERRAFORM_ARTIFACT_BUCKET_STATE_ADDRESS_PATTERN = /^google_storage_bucket\.atlas_search_artifacts\["([^"]+)"\]$/u; const SEARCH_TERRAFORM_DLQ_BUCKET_STATE_ADDRESS = 'google_storage_bucket.atlas_search_dlq[0]'; const SEARCH_TERRAFORM_PREREQUISITE_TARGET_KINDS = new Set(['secret-access']); const resolveSearchTerraformTaskQueueSummary = searchConfig => ({ location: normalizeOptionalString(searchConfig?.deploy?.taskQueue?.region) ?? normalizeOptionalString(searchConfig?.deploy?.cloudRun?.region) ?? 'europe-west1', name: normalizeOptionalString(searchConfig?.deploy?.taskQueue?.name) ?? ATLAS_SEARCH_TASK_QUEUE_NAME }); const resolveSearchTerraformWorkspaceName = terraformArtifact => { const workspaceName = normalizeOptionalString(terraformArtifact?.payload?.project_id); if (workspaceName) { return workspaceName; } throw new Error('Atlas search Terraform input must define payload.project_id before Terraform can select a workspace.'); }; const escapeTerraformString = value => String(value).replaceAll('\\', '\\\\').replaceAll('"', '\\"'); const readGcloudIamPolicy = (args, runGcloudFileCommandImpl = runGcloudFileCommand) => JSON.parse(runGcloudFileCommandImpl(args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })); const hasIamBinding = (policy, role, member) => (policy.bindings ?? []).some(binding => binding?.role === role && Array.isArray(binding.members) && binding.members.includes(member)); const renderSearchTerraformRootFiles = async (moduleSource, rootPath, options = {}) => { const templateFiles = await loadSearchTerraformRuntimeRootTemplateFiles(moduleSource, rootPath, options); const renderedFiles = renderSearchTerraformRootTemplateFiles(templateFiles, { __ATLAS_SEARCH_TERRAFORM_CONTRACT_VERSION__: SEARCH_TERRAFORM_CONTRACT_VERSION, __ATLAS_SEARCH_TERRAFORM_INPUT_VARIABLE__: SEARCH_TERRAFORM_INPUT_VARIABLE, __ATLAS_SEARCH_TERRAFORM_MODULE_SOURCE__: escapeTerraformString(moduleSource) }); return { ...renderedFiles }; }; export const resolveSearchTerraformConfig = searchConfig => ({ moduleSource: searchConfig?.deploy?.terraform?.moduleSource ?? DEFAULT_SEARCH_TERRAFORM_MODULE_SOURCE, rootDir: searchConfig?.deploy?.terraform?.rootDir ?? DEFAULT_SEARCH_TERRAFORM_ROOT_DIR }); export const resolveSearchTerraformRootPath = (searchConfig, cwd = process.cwd()) => resolveRootPath(path.join(resolveSearchTerraformConfig(searchConfig).rootDir, 'runtime'), cwd); export const createSearchTerraformWorkflowSummary = (searchConfig, cwd = process.cwd()) => { const terraformConfig = resolveSearchTerraformConfig(searchConfig); const eventarcCollectionPlans = getEnabledSearchEventarcCollectionPlans(searchConfig); const scheduledSourcePlans = getEnabledScheduledSearchSourcePlans(searchConfig); const taskQueueConfig = resolveSearchTerraformTaskQueueSummary(searchConfig); return { rootDir: terraformConfig.rootDir, rootPath: resolveSearchTerraformRootPath(searchConfig, cwd), moduleSource: terraformConfig.moduleSource, managedResources: [{ description: 'Secret Manager accessor bindings for the configured Atlas search Cloud Run runtime identity.', kind: 'secret-access', name: 'atlas-search-runtime-secrets' }, { description: 'Static Cloud Run backfill job definition managed through the Atlas search Terraform workflow.', kind: 'job', name: 'atlas-search-backfill' }, ...spreadIf(scheduledSourcePlans.length > 0, [{ description: 'Cloud Run job that executes scheduled Atlas search sources through the shared source adapter runtime.', kind: 'job', name: SEARCH_RESOURCE_NAMES.sourceRunnerJob }]), { description: 'Cloud Tasks queue that durably buffers Atlas search incremental sync work before atlas-search-sync processes it.', kind: 'task-queue', name: taskQueueConfig.name }, ...scheduledSourcePlans.map(sourcePlan => ({ description: 'Cloud Scheduler job that triggers atlas-search-source-runner for one configured scheduled source.', kind: 'scheduler-job', name: sourcePlan.schedulerJobName })), { description: 'Eventarc trigger service account used to deliver Firestore direct events into atlas-search-sync.', kind: 'service-account', name: 'atlas-search-eventarc' }, ...eventarcCollectionPlans.map(collectionPlan => ({ description: 'Firestore direct-event trigger routed to atlas-search-sync for the configured Atlas search collection.', kind: 'eventarc-trigger', name: collectionPlan.triggerName }))] }; }; export const ensureSearchTerraformRoot = async (searchConfig, cwd = process.cwd(), options = {}) => { const { existsSyncImpl = fs.existsSync, mkdirSyncImpl = fs.mkdirSync, readFileSyncImpl = fs.readFileSync, writeFileSyncImpl = fs.writeFileSync } = options; const workflowSummary = createSearchTerraformWorkflowSummary(searchConfig, cwd); const rootFiles = await renderSearchTerraformRootFiles(workflowSummary.moduleSource, workflowSummary.rootPath, options); 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 createSearchTerraformWorkflowCommand = (mode, terraformArtifact, options = {}) => { const targetArgs = (options.targets ?? []).map(target => `-target=${target}`); const baseArgs = [createTerraformStringVariableArgument(SEARCH_TERRAFORM_INPUT_VARIABLE, terraformArtifact.filePath), ...targetArgs, '-input=false']; if (mode === 'plan') { return ['plan', ...baseArgs]; } return [mode, '-auto-approve', ...baseArgs]; }; const resolveSearchTerraformApplyTargets = (targets, importResults, options = {}) => { if (!Array.isArray(targets)) { return undefined; } if (options.applyTargetsMode !== 'missing') { return targets; } const importStatusByAddress = new Map((importResults ?? []).map(result => [result.address, result.status])); return targets.filter(target => { const status = importStatusByAddress.get(target); return status === undefined || status === 'missing' || status === 'unknown'; }); }; const createSearchTerraformImportCommand = (terraformArtifact, target) => ['import', createTerraformStringVariableArgument(SEARCH_TERRAFORM_INPUT_VARIABLE, terraformArtifact.filePath), '-input=false', target.address, target.id]; const createSearchTerraformStateShowCommand = address => ['state', 'show', address]; const createSearchTerraformStateListCommand = () => ['state', 'list']; const parseSearchTerraformStateAddresses = stateListOutput => String(stateListOutput ?? '').split(/\r?\n/u).map(line => line.trim()).filter(Boolean); const parseSearchTerraformBucketLocation = stateShowOutput => { const locationMatch = String(stateShowOutput ?? '').match(/^location\s*=\s*"([^"]+)"$/mu); return locationMatch?.[1] ?? null; }; const parseSearchTerraformBucketName = stateShowOutput => { const nameMatch = String(stateShowOutput ?? '').match(/^name\s*=\s*"([^"]+)"$/mu); return nameMatch?.[1] ?? null; }; const mergeSearchTerraformArtifactBuckets = (configuredBuckets, existingBuckets) => { const bucketMap = new Map(); for (const bucket of configuredBuckets ?? []) { if (typeof bucket?.bucket_name !== 'string' || bucket.bucket_name.length === 0) { continue; } bucketMap.set(bucket.bucket_name, bucket); } for (const bucket of existingBuckets) { if (!bucketMap.has(bucket.bucket_name)) { bucketMap.set(bucket.bucket_name, bucket); } } return [...bucketMap.values()].sort((left, right) => left.bucket_name.localeCompare(right.bucket_name)); }; const resolveSearchTerraformCreateOnlyArtifactBuckets = async (workflowSummary, runTerraformCommandImpl) => { let stateAddressesOutput = ''; try { stateAddressesOutput = (await runTerraformCommandImpl(createSearchTerraformStateListCommand(), { captureOutput: true, cwd: workflowSummary.rootPath })) ?? ''; } catch (error) { if (SEARCH_TERRAFORM_STATE_NOT_FOUND_PATTERN.test(error.message)) { return []; } throw error; } const createOnlyBuckets = []; for (const stateAddress of parseSearchTerraformStateAddresses(stateAddressesOutput)) { const bucketName = stateAddress.match(SEARCH_TERRAFORM_ARTIFACT_BUCKET_STATE_ADDRESS_PATTERN)?.[1]; if (!bucketName) { continue; } const stateShowOutput = await runTerraformCommandImpl(createSearchTerraformStateShowCommand(stateAddress), { captureOutput: true, cwd: workflowSummary.rootPath }); createOnlyBuckets.push({ bucket_name: bucketName, location: parseSearchTerraformBucketLocation(stateShowOutput), uniform_bucket_level_access: true }); } return createOnlyBuckets; }; const resolveSearchTerraformCreateOnlyDlqBucket = async (workflowSummary, runTerraformCommandImpl) => { try { const stateShowOutput = await runTerraformCommandImpl(createSearchTerraformStateShowCommand(SEARCH_TERRAFORM_DLQ_BUCKET_STATE_ADDRESS), { captureOutput: true, cwd: workflowSummary.rootPath }); const bucketName = parseSearchTerraformBucketName(stateShowOutput); if (!bucketName) { return null; } return { bucket_name: bucketName, location: parseSearchTerraformBucketLocation(stateShowOutput), uniform_bucket_level_access: true }; } catch (error) { if (SEARCH_TERRAFORM_STATE_NOT_FOUND_PATTERN.test(error.message)) { return null; } throw error; } }; const preserveSearchTerraformCreateOnlyBuckets = async (terraformArtifact, workflowSummary, dependencies = {}) => { const runTerraformCommandImpl = dependencies.runTerraformCommand ?? runTerraformCommand; const writeJsonFileImpl = dependencies.writeJsonFile ?? writeJsonFile; const payload = terraformArtifact?.payload; if (!payload || typeof payload !== 'object') { return terraformArtifact; } const existingArtifactBuckets = await resolveSearchTerraformCreateOnlyArtifactBuckets(workflowSummary, runTerraformCommandImpl); const existingDlqBucket = await resolveSearchTerraformCreateOnlyDlqBucket(workflowSummary, runTerraformCommandImpl); const mergedArtifactBuckets = mergeSearchTerraformArtifactBuckets(payload.artifact_buckets, existingArtifactBuckets); const shouldPreserveDlqBucket = existingDlqBucket !== null && payload.dlq_bucket?.bucket_name !== existingDlqBucket.bucket_name; if (JSON.stringify(mergedArtifactBuckets) === JSON.stringify(payload.artifact_buckets ?? []) && !shouldPreserveDlqBucket) { return terraformArtifact; } const nextPayload = { ...payload, artifact_buckets: mergedArtifactBuckets, ...(() => { if (!shouldPreserveDlqBucket) { return {}; } return { dlq_bucket: { ...(payload.dlq_bucket ?? {}), bucket_name: existingDlqBucket.bucket_name, location: existingDlqBucket.location ?? payload.dlq_bucket?.location }, ...spreadIf(payload.dlq_bucket_access, { dlq_bucket_access: { ...payload.dlq_bucket_access, bucket_name: existingDlqBucket.bucket_name } }) }; })() }; writeJsonFileImpl(terraformArtifact.filePath, nextPayload); return { ...terraformArtifact, payload: nextPayload }; }; const doesSecretAccessorBindingExist = (secretName, projectId, role, member, runGcloudFileCommandImpl = runGcloudFileCommand) => { const policy = readGcloudIamPolicy(['secrets', 'get-iam-policy', secretName, `--project=${projectId}`, '--format=json'], runGcloudFileCommandImpl); return hasIamBinding(policy, role, member); }; const createSearchTerraformImportResult = (target, status) => ({ address: target.address, kind: target.kind, name: target.name, status }); const getSearchTerraformImportTargets = terraformArtifact => { const payload = terraformArtifact?.payload; if (!payload || typeof payload !== 'object') { return []; } const projectId = payload.project_id; const backfillJob = payload.backfill_job; const firestoreEventarc = payload.firestore_eventarc; const runtimeSecretAccess = payload.runtime_secret_access; const sourceRunnerJob = payload.source_runner_job; const sourceScheduler = payload.source_scheduler; const syncTaskQueue = payload.sync_task_queue; const eventarcTriggerRegion = firestoreEventarc?.trigger_region ?? firestoreEventarc?.region; const targets = []; if (typeof projectId === 'string' && typeof runtimeSecretAccess?.service_account_email === 'string' && typeof runtimeSecretAccess?.role === 'string' && Array.isArray(runtimeSecretAccess?.secret_names)) { for (const secretName of runtimeSecretAccess.secret_names) { if (typeof secretName !== 'string') { continue; } const member = `serviceAccount:${runtimeSecretAccess.service_account_email}`; targets.push({ address: `google_secret_manager_secret_iam_member.atlas_search_runtime["${secretName}"]`, describeArgs: ['secrets', 'get-iam-policy', secretName, `--project=${projectId}`, '--format=json'], existsImpl: runGcloudFileCommandImpl => doesSecretAccessorBindingExist(secretName, projectId, runtimeSecretAccess.role, member, runGcloudFileCommandImpl), id: `projects/${projectId}/secrets/${secretName} ${runtimeSecretAccess.role} ${member}`, kind: 'secret-access', name: secretName }); } } if (typeof projectId === 'string' && typeof backfillJob?.job_name === 'string' && typeof backfillJob?.region === 'string') { targets.push({ address: 'module.atlas_search_backfill_job[0].google_cloud_run_v2_job.this', describeArgs: ['run', 'jobs', 'describe', backfillJob.job_name, `--project=${projectId}`, `--region=${backfillJob.region}`, '--format="value(name)"'], id: `projects/${projectId}/locations/${backfillJob.region}/jobs/${backfillJob.job_name}`, kind: 'job', name: backfillJob.job_name }); } if (typeof projectId === 'string' && typeof sourceRunnerJob?.job_name === 'string' && typeof sourceRunnerJob?.region === 'string') { targets.push({ address: 'module.atlas_search_source_runner_job[0].google_cloud_run_v2_job.this', describeArgs: ['run', 'jobs', 'describe', sourceRunnerJob.job_name, `--project=${projectId}`, `--region=${sourceRunnerJob.region}`, '--format="value(name)"'], id: `projects/${projectId}/locations/${sourceRunnerJob.region}/jobs/${sourceRunnerJob.job_name}`, kind: 'job', name: sourceRunnerJob.job_name }); } if (typeof projectId === 'string' && typeof syncTaskQueue?.name === 'string' && typeof syncTaskQueue?.location === 'string') { targets.push({ address: 'google_cloud_tasks_queue.atlas_search_sync[0]', describeArgs: ['tasks', 'queues', 'describe', syncTaskQueue.name, `--location=${syncTaskQueue.location}`, `--project=${projectId}`, '--format="value(name)"'], id: `projects/${projectId}/locations/${syncTaskQueue.location}/queues/${syncTaskQueue.name}`, kind: 'task-queue', name: syncTaskQueue.name }); } if (typeof projectId === 'string' && typeof firestoreEventarc?.trigger_service_account?.account_id === 'string') { const serviceAccountEmail = `${firestoreEventarc.trigger_service_account.account_id}@${projectId}.iam.gserviceaccount.com`; targets.push({ address: 'google_service_account.atlas_search_eventarc[0]', describeArgs: ['iam', 'service-accounts', 'describe', serviceAccountEmail, `--project=${projectId}`, '--format="value(email)"'], id: `projects/${projectId}/serviceAccounts/${serviceAccountEmail}`, kind: 'service-account', name: serviceAccountEmail }); } if (typeof projectId === 'string' && typeof sourceScheduler?.region === 'string' && Array.isArray(sourceScheduler.jobs)) { for (const schedulerJob of sourceScheduler.jobs) { if (typeof schedulerJob?.name !== 'string') { continue; } targets.push({ address: `google_cloud_scheduler_job.atlas_search_source_runner["${schedulerJob.name}"]`, describeArgs: ['scheduler', 'jobs', 'describe', schedulerJob.name, `--location=${sourceScheduler.region}`, `--project=${projectId}`, '--format="value(name)"'], id: `projects/${projectId}/locations/${sourceScheduler.region}/jobs/${schedulerJob.name}`, kind: 'scheduler-job', name: schedulerJob.name }); } } if (typeof projectId === 'string' && typeof eventarcTriggerRegion === 'string' && Array.isArray(firestoreEventarc.collections)) { for (const collection of firestoreEventarc.collections) { if (typeof collection?.collection_name !== 'string' || typeof collection?.trigger_name !== 'string') { continue; } targets.push({ address: `google_eventarc_trigger.atlas_search_firestore["${collection.collection_name}"]`, describeArgs: ['eventarc', 'triggers', 'describe', collection.trigger_name, `--project=${projectId}`, `--location=${eventarcTriggerRegion}`, '--format="value(name)"'], id: `projects/${projectId}/locations/${eventarcTriggerRegion}/triggers/${collection.trigger_name}`, kind: 'eventarc-trigger', name: collection.trigger_name }); } } return targets; }; const normalizeCommandErrorMessage = error => { const stderr = error?.stderr && Buffer.isBuffer(error.stderr) ? error.stderr.toString('utf8') : typeof error?.stderr === 'string' ? error.stderr : ''; return [error?.message, stderr].filter(Boolean).join('\n'); }; const isGcloudPermissionDeniedError = error => GCLOUD_PERMISSION_DENIED_ERROR_PATTERN.test(normalizeCommandErrorMessage(error)); const doesSearchTerraformImportTargetExist = (target, runGcloudFileCommandImpl = runGcloudFileCommand) => { try { if (typeof target.existsImpl === 'function') { return target.existsImpl(runGcloudFileCommandImpl) ? 'exists' : 'missing'; } runGcloudFileCommandImpl(target.describeArgs, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }); return 'exists'; } catch (error) { const message = normalizeCommandErrorMessage(error); if (isGcloudResourceNotFoundError(error)) { return 'missing'; } if (isGcloudPermissionDeniedError(error)) { return 'unknown'; } throw new Error(`Could not inspect existing Atlas search ${target.kind} ${target.name}. ${message}`); } }; export const importExistingSearchTerraformResources = async (terraformArtifact, workflowSummary, dependencies = {}) => { const importResults = []; const runTerraformCommandImpl = dependencies.runTerraformCommand ?? runTerraformCommand; const runGcloudFileCommandImpl = dependencies.runGcloudFileCommand ?? runGcloudFileCommand; const selectedImportTargets = Array.isArray(dependencies.importTargetAddresses) ? new Set(dependencies.importTargetAddresses) : null; for (const target of getSearchTerraformImportTargets(terraformArtifact).filter(importTarget => !selectedImportTargets || selectedImportTargets.has(importTarget.address))) { const existenceStatus = doesSearchTerraformImportTargetExist(target, runGcloudFileCommandImpl); if (existenceStatus === 'missing') { importResults.push(createSearchTerraformImportResult(target, 'missing')); continue; } if (existenceStatus === 'unknown') { importResults.push(createSearchTerraformImportResult(target, 'unknown')); continue; } try { await runTerraformCommandImpl(createSearchTerraformStateShowCommand(target.address), { captureOutput: true, cwd: workflowSummary.rootPath }); importResults.push(createSearchTerraformImportResult(target, 'already-managed')); continue; } catch (error) { if (!SEARCH_TERRAFORM_STATE_NOT_FOUND_PATTERN.test(error.message)) { throw error; } } try { await runTerraformCommandImpl(createSearchTerraformImportCommand(terraformArtifact, target), { captureOutput: true, cwd: workflowSummary.rootPath }); importResults.push(createSearchTerraformImportResult(target, 'imported')); } catch (error) { if (SEARCH_TERRAFORM_IMPORT_ALREADY_MANAGED_PATTERN.test(error.message)) { importResults.push(createSearchTerraformImportResult(target, 'already-managed')); continue; } throw error; } } return importResults; }; export const getSearchTerraformPrerequisiteTargets = terraformArtifact => getSearchTerraformImportTargets(terraformArtifact).filter(target => SEARCH_TERRAFORM_PREREQUISITE_TARGET_KINDS.has(target.kind)).map(target => target.address); export const runSearchTerraformWorkflow = async (searchConfig, terraformArtifact, options = {}, dependencies = {}, cwd = process.cwd()) => { const ensureTerraformRoot = dependencies.ensureSearchTerraformRoot ?? ensureSearchTerraformRoot; const ensureTerraformWorkspaceImpl = dependencies.ensureTerraformWorkspace ?? ensureTerraformWorkspace; const runTerraformCommandImpl = dependencies.runTerraformCommand ?? runTerraformCommand; const workflowSummary = await ensureTerraformRoot(searchConfig, cwd, dependencies); const mode = options.mode ?? (options.dryRun ? 'plan' : 'apply'); const initArgs = ['init', '-upgrade', '-input=false']; const importResults = []; await runTerraformCommandImpl(initArgs, { cwd: workflowSummary.rootPath, stdio: 'inherit' }); const workspaceSelection = await ensureTerraformWorkspaceImpl(resolveSearchTerraformWorkspaceName(terraformArtifact), { cwd: workflowSummary.rootPath }, { runTerraformCommand: runTerraformCommandImpl }); const effectiveTerraformArtifact = mode === 'destroy' ? terraformArtifact : await preserveSearchTerraformCreateOnlyBuckets(terraformArtifact, workflowSummary, { runTerraformCommand: runTerraformCommandImpl, writeJsonFile: dependencies.writeJsonFile }); if (mode !== 'plan') { importResults.push(...(await importExistingSearchTerraformResources(effectiveTerraformArtifact, workflowSummary, { importTargetAddresses: options.targets, runGcloudFileCommand: dependencies.runGcloudFileCommand, runTerraformCommand: runTerraformCommandImpl }))); } const applyTargets = resolveSearchTerraformApplyTargets(options.targets, importResults, options); if (mode !== 'plan' && Array.isArray(options.targets) && applyTargets.length === 0) { return { ...workflowSummary, commands: [initArgs, workspaceSelection.command], importResults, mode, workspaceName: workspaceSelection.name, skippedApply: true }; } const workflowArgs = createSearchTerraformWorkflowCommand(mode, effectiveTerraformArtifact, { targets: applyTargets }); await runTerraformCommandImpl(workflowArgs, { cwd: workflowSummary.rootPath, stdio: 'inherit' }); return { ...workflowSummary, commands: [initArgs, workspaceSelection.command, workflowArgs], importResults, mode, workspaceName: workspaceSelection.name }; };