@puls-atlas/cli
Version:
The Puls Atlas CLI tool for managing Atlas projects
159 lines • 6.95 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 { deleteFirestorePathRecursively } from '../../utils/firestore.js';
import { resolveSyncCloudRunDeployConfig } from './deploymentConfig.js';
import { SYNC_BACKFILL_JOB_NAME, SYNC_SERVICE_NAME } from './resourceNames.js';
import { DEFAULT_SYNC_STATE_COLLECTION_PATH } from './config/syncConfig.js';
import { writeSyncTerraformArtifact } from './terraformAdapter.js';
import { formatShellCommand, isGcloudResourceNotFoundError, logger, runGcloudFileCommand } from '../../utils/index.js';
import { getAtlasFeatureCachePath, getAtlasGeneratedArtifactsDir, getAtlasGeneratedFeatureConfigPath, getAtlasGeneratedTerraformDir } from '../../utils/atlas.js';
import { createSyncTerraformWorkflowSummary, runSyncTerraformWorkflow } from './terraformWorkflow.js';
export const createSyncDestroyPlan = (context, cwd = process.cwd()) => {
const commands = [];
const {
region
} = resolveSyncCloudRunDeployConfig(context);
const syncStateCollectionPath = context.config?.deploy?.syncState?.collectionPath ?? DEFAULT_SYNC_STATE_COLLECTION_PATH;
const serviceArgs = ['run', 'services', 'delete', SYNC_SERVICE_NAME, `--project=${context.projectId}`, '--platform=managed', `--region=${region}`, '--quiet'];
commands.push({
args: serviceArgs,
command: formatShellCommand(['gcloud', ...serviceArgs]),
description: `Delete Cloud Run service ${SYNC_SERVICE_NAME}`,
executable: 'gcloud',
kind: 'service',
tolerateNotFound: true
});
const jobArgs = ['run', 'jobs', 'delete', SYNC_BACKFILL_JOB_NAME, `--project=${context.projectId}`, `--region=${region}`, '--quiet'];
commands.push({
args: jobArgs,
command: formatShellCommand(['gcloud', ...jobArgs]),
description: `Delete Cloud Run job ${SYNC_BACKFILL_JOB_NAME}`,
executable: 'gcloud',
kind: 'job',
tolerateNotFound: true
});
return {
commands,
projectId: context.projectId,
firestoreCleanupPaths: [syncStateCollectionPath],
terraform: createSyncTerraformWorkflowSummary(context.config, cwd),
localCleanupPaths: [getAtlasFeatureCachePath('sync', context.projectId, cwd), getAtlasGeneratedFeatureConfigPath('sync', context.projectId, cwd), path.join(getAtlasGeneratedArtifactsDir(cwd), 'sync', 'mappers', context.projectId), path.join(getAtlasGeneratedTerraformDir(cwd), 'sync', `${context.projectId}.tfvars.json`)]
};
};
export const executeSyncDestroyPlan = async (destroyPlan, options = {}) => {
const results = [];
const {
runCommand = nodeExecFileSync
} = options;
const deleteFirestorePathRecursivelyImpl = options.deleteFirestorePathRecursivelyImpl ?? deleteFirestorePathRecursively;
for (const command of destroyPlan.commands) {
try {
runGcloudFileCommand(command.args, {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe']
}, runCommand);
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 plan', [{
label: 'Project',
value: destroyPlan.projectId
}, {
label: 'Terraform root',
value: destroyPlan.terraform.rootPath
}, {
label: 'Terraform module source',
value: destroyPlan.terraform.moduleSource
}]);
logger.summary('Planned remote destroy commands', destroyPlan.commands.map(command => chalk => chalk.gray(command.command)), {
emptyMessage: 'No remote Atlas sync 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('sync', options, {
cwd: process.cwd()
});
spinner = logger.spinner('Preparing Atlas sync destroy plan...');
const destroyPlan = createSyncDestroyPlan(context, process.cwd());
spinner.succeed('Atlas sync destroy plan is ready.');
logDestroyPlan(destroyPlan);
const confirmation = await inquirer.prompt([{
default: false,
type: 'confirm',
name: 'shouldDestroy',
message: 'Continue destroying Atlas sync Terraform-managed Cloud Run resources, ' + 'persistent Firestore sync state, and local generated sync artifacts?'
}]);
if (!confirmation.shouldDestroy) {
logger.warning('Atlas sync destroy aborted.');
return;
}
const deployConfig = resolveSyncCloudRunDeployConfig(context);
const terraformArtifact = writeSyncTerraformArtifact(context, deployConfig.runtimeConfigUri, deployConfig.mapperManifestUri, {}, process.cwd());
spinner = logger.spinner('Destroying Atlas sync Terraform-managed resources...');
await runSyncTerraformWorkflow(context.config, terraformArtifact, {
mode: 'destroy'
}, {}, process.cwd());
spinner.succeed('Atlas sync Terraform-managed resources destroyed.');
spinner = logger.spinner('Destroying Atlas sync Cloud Run resources...');
const destroyResults = await executeSyncDestroyPlan(destroyPlan);
spinner.succeed('Atlas sync Cloud Run resources destroyed.');
const deletedCount = destroyResults.filter(r => r.status === 'deleted').length;
const notFoundCount = destroyResults.filter(r => r.status === 'not-found').length;
logger.summary('Destroy results', [{
label: 'Deleted',
value: String(deletedCount)
}, ...(notFoundCount > 0 ? [{
label: 'Not found (skipped)',
value: String(notFoundCount)
}] : [])]);
logger.info('Atlas sync destroy complete. All Terraform-managed resources, Cloud Run services, ' + 'Firestore sync state, and local generated artifacts have been removed.');
} catch (error) {
spinner?.fail('Atlas sync destroy failed.');
logger.error(error.message);
}
};