@puls-atlas/cli
Version:
The Puls Atlas CLI tool for managing Atlas projects
159 lines • 6.58 kB
JavaScript
import fs from 'fs';
import path from 'path';
import inquirer from 'inquirer';
import { execFileSync as nodeExecFileSync } from 'child_process';
import * as features from '../../utils/feature.js';
import { SEARCH_RESOURCE_NAMES } from './resourceNames.js';
import { getSearchProvider } from './providers/index.js';
import { resolveSearchCloudRunDeployConfig } from './planning.js';
import { deleteFirestorePathRecursively } from '../../utils/firestore.js';
import { formatShellCommand, isGcloudResourceNotFoundError, logger, runGcloudFileCommand } from '../../utils/index.js';
import { getAtlasFeatureCachePath, getAtlasGeneratedArtifactsDir, getAtlasGeneratedFeatureConfigPath } from '../../utils/atlas.js';
import { getSearchTerraformBackfillJobArtifactPath, writeSearchTerraformBackfillJobArtifact } from './terraformAdapter.js';
import { createSearchTerraformWorkflowSummary, runSearchTerraformWorkflow } from './terraformWorkflow.js';
const SEARCH_STATE_DOCUMENT_PATH = 'sync/search';
export const createSearchDestroyPlan = (context, cwd = process.cwd()) => {
const {
region
} = resolveSearchCloudRunDeployConfig(context);
const commands = [];
for (const serviceName of [SEARCH_RESOURCE_NAMES.searchSyncService, SEARCH_RESOURCE_NAMES.searchApiService]) {
const args = ['run', 'services', 'delete', serviceName, `--project=${context.projectId}`, '--platform=managed', `--region=${region}`, '--quiet'];
commands.push({
args,
command: formatShellCommand(['gcloud', ...args]),
description: `Delete Cloud Run service ${serviceName}`,
executable: 'gcloud',
kind: 'service',
tolerateNotFound: true
});
}
return {
commands,
firestoreCleanupPaths: [SEARCH_STATE_DOCUMENT_PATH],
projectId: context.projectId,
localCleanupPaths: [getAtlasFeatureCachePath('search', context.projectId, cwd), getAtlasGeneratedFeatureConfigPath('search', context.projectId, cwd), path.join(getAtlasGeneratedArtifactsDir(cwd), 'search', 'mappers', context.projectId), path.join(cwd, '.atlas', 'artifacts', 'search', 'mappers', context.projectId), getSearchTerraformBackfillJobArtifactPath(context.projectId, cwd), path.join(cwd, '.atlas', 'terraform', 'search', `search.${context.projectId}.tfvars.json`)],
terraform: createSearchTerraformWorkflowSummary(context.config, cwd)
};
};
export const executeSearchDestroyPlan = async (destroyPlan, options = {}) => {
const {
runCommand = nodeExecFileSync
} = options;
const deleteFirestorePathRecursivelyImpl = options.deleteFirestorePathRecursivelyImpl ?? deleteFirestorePathRecursively;
const results = [];
for (const command of destroyPlan.commands) {
try {
if (command.executable === 'gcloud') {
runGcloudFileCommand(command.args, {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe']
}, runCommand);
} else {
runCommand(command.executable, command.args, {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe']
});
}
results.push({
kind: command.kind,
status: 'deleted'
});
} catch (error) {
if (command.tolerateNotFound && isGcloudResourceNotFoundError(error)) {
results.push({
kind: command.kind,
status: 'not-found'
});
continue;
}
throw error;
}
}
for (const firestorePath of destroyPlan.firestoreCleanupPaths ?? []) {
await deleteFirestorePathRecursivelyImpl({
firestorePath,
projectId: destroyPlan.projectId
});
results.push({
kind: 'firestore',
status: 'deleted'
});
}
for (const cleanupPath of destroyPlan.localCleanupPaths) {
if (!fs.existsSync(cleanupPath)) {
continue;
}
fs.rmSync(cleanupPath, {
force: true,
recursive: true
});
}
return results;
};
const logDestroyPlan = destroyPlan => {
logger.summary('Destroy summary', [{
label: 'Project',
value: destroyPlan.projectId
}, {
label: 'Terraform root',
value: destroyPlan.terraform.rootPath
}, {
label: 'Terraform module source',
value: destroyPlan.terraform.moduleSource
}, {
label: 'Terraform-managed job',
value: SEARCH_RESOURCE_NAMES.backfillJob
}, {
label: 'Firestore cleanup',
value: (destroyPlan.firestoreCleanupPaths ?? []).join(', ') || 'none'
}]);
logger.summary('Planned remote destroy commands', destroyPlan.commands.map(command => chalk => chalk.gray(command.command)), {
emptyMessage: 'No remote Atlas search commands need to run.'
});
logger.summary('Local cleanup paths', destroyPlan.localCleanupPaths.map(cleanupPath => chalk => chalk.gray(cleanupPath)), {
emptyMessage: 'No local cleanup paths are configured.'
});
logger.summary('Firestore cleanup paths', (destroyPlan.firestoreCleanupPaths ?? []).map(firestorePath => chalk => chalk.gray(firestorePath)), {
emptyMessage: 'No Firestore cleanup paths are configured.'
});
};
export default async options => {
let spinner;
try {
const context = await features.loadFeatureContext('search', options, {
cwd: process.cwd()
});
spinner = logger.spinner('Preparing Atlas search destroy plan...');
const provider = getSearchProvider(context.config.provider);
const destroyPlan = createSearchDestroyPlan(context, process.cwd());
spinner.succeed('Atlas search destroy plan is ready.');
logDestroyPlan(destroyPlan);
const confirmation = await inquirer.prompt([{
default: false,
type: 'confirm',
name: 'shouldDestroy',
message: 'Continue destroying Atlas search Terraform, Eventarc-managed Cloud Run resources, persistent Firestore search state, and local generated search artifacts?'
}]);
if (!confirmation.shouldDestroy) {
logger.warning('Atlas search destroy aborted.');
return;
}
const destroySpinner = logger.spinner('Destroying Atlas search resources...');
const terraformArtifact = writeSearchTerraformBackfillJobArtifact(context, {}, process.cwd());
await runSearchTerraformWorkflow(context.config, terraformArtifact, {
mode: 'destroy'
}, {}, process.cwd());
await executeSearchDestroyPlan(destroyPlan);
destroySpinner.succeed('Atlas search resources destroyed successfully.');
if (provider.getDestroyNotice) {
logger.warning(provider.getDestroyNotice(context));
}
} catch (error) {
if (spinner) {
spinner.fail('Failed to destroy Atlas search resources.');
}
logger.error(error.message, false);
process.exit(1);
}
};