UNPKG

@puls-atlas/cli

Version:

The Puls Atlas CLI tool for managing Atlas projects

159 lines 6.95 kB
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); } };