@puls-atlas/cli
Version:
The Puls Atlas CLI tool for managing Atlas projects
449 lines • 18.5 kB
JavaScript
import { execFileSync } from 'child_process';
import { isPlainObject } from 'es-toolkit/compat';
import * as features from '../../utils/feature.js';
import { compileSyncPlan } from './config/syncConfig.js';
import { executeSyncAdminRequest } from './syncApiAdmin.js';
import { normalizeOptionalString } from '../../utils/value.js';
import { materializeGcloudCommandFlagsFile } from '../../utils/gcloud.js';
import { getCommandErrorMessage, isGcloudResourceNotFoundError, logger, parseGcloudJsonOutput, runGcloudFileCommand } from '../../utils/index.js';
import { createSyncBackfillJobExecutionCommand, ensureSyncBackfillJobExists } from './backfillJob.js';
const SUPPORTED_RUN_ACTIONS = new Set(['backfill', 'incremental']);
const DEFAULT_FOLLOW_TIMEOUT_SECONDS = 1800;
const DEFAULT_FOLLOW_POLL_INTERVAL_MS = 5000;
const EXECUTION_FAILURE_DETAILS_LIMIT = 5;
const normalizeRunAction = action => normalizeOptionalString(action)?.toLowerCase() ?? null;
const parseRequestedSyncWorkloads = (workloadsArgument, availableWorkloads) => {
const normalizedArgument = normalizeOptionalString(workloadsArgument);
const normalizedAvailableWorkloads = Array.isArray(availableWorkloads) ? availableWorkloads.filter(Boolean) : [];
if (!normalizedArgument) {
if (normalizedAvailableWorkloads.length === 0) {
throw new Error('Atlas sync run backfill requires at least one enabled workload in the selected project.');
}
return normalizedAvailableWorkloads;
}
const requestedWorkloads = [...new Set(normalizedArgument.split(',').map(value => value.trim()).filter(Boolean))];
const availableWorkloadSet = new Set(normalizedAvailableWorkloads);
const unknownWorkloads = requestedWorkloads.filter(workloadKey => !availableWorkloadSet.has(workloadKey));
if (unknownWorkloads.length > 0) {
throw new Error(`Atlas sync run backfill received unknown or disabled workload keys: ${unknownWorkloads.join(', ')}.`);
}
return requestedWorkloads;
};
const normalizeTimeoutSeconds = timeout => {
if (timeout === undefined || timeout === null || timeout === '') {
return DEFAULT_FOLLOW_TIMEOUT_SECONDS;
}
const parsedValue = Number.parseInt(timeout, 10);
if (!Number.isInteger(parsedValue) || parsedValue <= 0) {
throw new Error('Atlas sync run requires --timeout <seconds> to be a positive integer.');
}
return parsedValue;
};
const logSyncOperation = (loggerImpl, context, action, command, workloadKeys) => {
loggerImpl.summary('Atlas sync run', [{
label: 'Project',
value: context.projectId
}, context.environment ? {
label: 'Environment',
value: context.environment
} : null, {
label: 'Config',
value: context.configPath
}, {
label: 'Action',
value: action
}, {
label: 'Workloads',
value: workloadKeys.join(', ')
}, {
label: 'Command',
value: command.command
}]);
};
export const runSyncBackfillOperation = async (action, workloadsArgument, commandOptions = {}, dependencies = {}, cwd = process.cwd()) => {
const options = commandOptions ?? {};
const compileSyncPlanImpl = dependencies.compileSyncPlanImpl ?? compileSyncPlan;
const ensureSyncBackfillJobExistsImpl = dependencies.ensureSyncBackfillJobExists ?? ensureSyncBackfillJobExists;
const exit = dependencies.exit ?? (code => process.exit(code));
const loadFeatureContext = dependencies.loadFeatureContext ?? features.loadFeatureContext;
const loggerImpl = dependencies.logger ?? logger;
const runCommand = dependencies.runCommand ?? execFileSync;
let spinner;
try {
spinner = loggerImpl.spinner(options.dryRun ? 'Preparing Atlas sync backfill command...' : 'Starting Atlas sync backfill...');
const context = await loadFeatureContext('sync', options, {
cwd
});
const syncPlan = compileSyncPlanImpl(context.config);
const workloadKeys = parseRequestedSyncWorkloads(workloadsArgument, syncPlan.pipelines.map(pipeline => pipeline.workloadKey));
if (!options.dryRun) {
ensureSyncBackfillJobExistsImpl(context, {
runCommand
});
}
const command = createSyncBackfillJobExecutionCommand({
context,
workloadKeys
});
if (options.dryRun) {
spinner.succeed('Atlas sync backfill command is ready.');
logSyncOperation(loggerImpl, context, action, command, workloadKeys);
loggerImpl.info('Dry run requested: no remote changes were made.');
return {
...command,
status: 'dry-run',
workloadKeys
};
}
const materializedFlagsFile = materializeGcloudCommandFlagsFile(command);
try {
runGcloudFileCommand(materializedFlagsFile?.args ?? command.args, {
stdio: 'inherit'
}, runCommand);
} finally {
materializedFlagsFile?.cleanup();
}
spinner.succeed('Atlas sync backfill started.');
logSyncOperation(loggerImpl, context, action, command, workloadKeys);
return {
...command,
status: 'started',
workloadKeys
};
} catch (error) {
if (spinner) {
spinner.fail('Failed to start Atlas sync backfill.');
}
loggerImpl.error(error.message, false);
return exit(1);
}
};
export const runSyncIncrementalOperation = async (action, commandOptions = {}, dependencies = {}, cwd = process.cwd()) => {
const options = commandOptions ?? {};
const executeSyncAdminRequestImpl = dependencies.executeSyncAdminRequest ?? executeSyncAdminRequest;
const exit = dependencies.exit ?? (code => process.exit(code));
const loadFeatureContext = dependencies.loadFeatureContext ?? features.loadFeatureContext;
const loggerImpl = dependencies.logger ?? logger;
try {
const workloadKey = normalizeOptionalString(options.workloadKey);
const documentId = normalizeOptionalString(options.documentId);
if (!workloadKey) {
throw new Error('Atlas sync run incremental requires --workload-key <workloadKey>.');
}
if (!documentId) {
throw new Error('Atlas sync run incremental requires --document-id <documentId>.');
}
const spinner = loggerImpl.spinner('Triggering Atlas sync incremental run...');
try {
const context = await loadFeatureContext('sync', options, {
cwd
});
const result = await executeSyncAdminRequestImpl(context, {
method: 'POST',
path: '/admin/run/incremental',
payload: {
workloadKey,
sourceDocumentId: documentId
}
});
spinner.succeed('Atlas sync incremental run triggered.');
loggerImpl.summary('Incremental run summary', [{
label: 'Project',
value: context.projectId
}, context.environment ? {
label: 'Environment',
value: context.environment
} : null, {
label: 'Workload key',
value: result.workloadKey ?? workloadKey
}, {
label: 'Source document',
value: result.sourceDocumentId ?? documentId
}, {
label: 'Task name',
value: result.taskName ?? 'unknown'
}]);
return result;
} catch (error) {
spinner.fail('Atlas sync incremental run failed.');
throw error;
}
} catch (error) {
loggerImpl.error(error.message, false);
return exit(1);
}
};
const resolveExecutionCondition = (execution, conditionType) => (execution?.status?.conditions ?? []).find(condition => condition?.type?.toLowerCase() === conditionType.toLowerCase()) ?? null;
const createExecutionSummary = execution => {
const completedCondition = resolveExecutionCondition(execution, 'Completed');
const cancelledCondition = resolveExecutionCondition(execution, 'Cancelled');
const failedCount = Number.parseInt(execution?.status?.failedCount ?? 0, 10) || 0;
const succeededCount = Number.parseInt(execution?.status?.succeededCount ?? 0, 10) || 0;
const stateReason = completedCondition?.reason ?? cancelledCondition?.reason ?? null;
if (cancelledCondition?.status === 'True') {
return {
failedCount,
reason: stateReason,
state: 'failed',
succeededCount
};
}
if (completedCondition?.status === 'True') {
if (failedCount > 0 || /fail|error|cancel/i.test(stateReason ?? '')) {
return {
failedCount,
reason: stateReason,
state: 'failed',
succeededCount
};
}
return {
failedCount,
reason: stateReason,
state: 'completed',
succeededCount
};
}
return {
failedCount,
reason: stateReason,
state: 'running',
succeededCount
};
};
const parseExecutionNameFromList = output => {
const normalized = normalizeOptionalString(output);
if (!normalized) {
return null;
}
return normalized.split(/\r?\n/).map(value => value.trim()).find(Boolean) ?? null;
};
const sleep = timeoutInMilliseconds => new Promise(resolve => {
setTimeout(resolve, timeoutInMilliseconds);
});
const normalizeFollowTimestamp = value => {
if (value instanceof Date) {
return value.getTime();
}
const normalizedValue = Number(value);
if (!Number.isFinite(normalizedValue)) {
throw new Error('Atlas sync run follow requires a clock that returns a Date or millisecond timestamp.');
}
return normalizedValue;
};
const normalizeFailureDetailValue = value => {
if (value === null || value === undefined) {
return [];
}
if (Array.isArray(value)) {
return value.flatMap(normalizeFailureDetailValue);
}
if (typeof value === 'string') {
return [value];
}
if (isPlainObject(value)) {
if (normalizeOptionalString(value.message)) {
return [value.message.trim()];
}
try {
return [JSON.stringify(value)];
} catch {
return [];
}
}
return [String(value)];
};
const extractExecutionFailureDetails = logsPayload => {
const details = [];
const seen = new Set();
const logEntries = Array.isArray(logsPayload) ? logsPayload : [logsPayload];
for (const logEntry of logEntries) {
const severity = normalizeOptionalString(logEntry?.severity);
const candidates = [...normalizeFailureDetailValue(logEntry?.jsonPayload?.error?.details), ...normalizeFailureDetailValue(logEntry?.jsonPayload?.error?.message), ...normalizeFailureDetailValue(logEntry?.jsonPayload?.message), ...normalizeFailureDetailValue(logEntry?.textPayload)];
for (const candidate of candidates) {
const normalized = candidate.replace(/\s+/g, ' ').trim();
if (normalized.length === 0) {
continue;
}
const formatted = severity ? `${severity}: ${normalized}` : normalized;
if (seen.has(formatted)) {
continue;
}
seen.add(formatted);
details.push(formatted);
if (details.length >= EXECUTION_FAILURE_DETAILS_LIMIT) {
return details;
}
}
}
return details;
};
const listLatestExecutionName = (operationResult, dependencies = {}) => {
const runGcloudFileCommandImpl = dependencies.runGcloudFileCommand ?? runGcloudFileCommand;
try {
const output = runGcloudFileCommandImpl(['run', 'jobs', 'executions', 'list', `--job=${operationResult.jobName}`, `--project=${operationResult.projectId}`, `--region=${operationResult.region}`, '--sort-by=~metadata.creationTimestamp', '--limit=1', '--format="value(metadata.name)"'], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe']
}, dependencies.runCommand);
return parseExecutionNameFromList(output);
} catch (error) {
if (isGcloudResourceNotFoundError(error)) {
return null;
}
throw new Error(`Could not list Atlas sync run executions for ${operationResult.jobName}. ${getCommandErrorMessage(error).trim()}`);
}
};
const describeExecution = (operationResult, executionName, dependencies = {}) => {
const runGcloudFileCommandImpl = dependencies.runGcloudFileCommand ?? runGcloudFileCommand;
try {
const output = runGcloudFileCommandImpl(['run', 'jobs', 'executions', 'describe', executionName, `--project=${operationResult.projectId}`, `--region=${operationResult.region}`, '--format=json'], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe']
}, dependencies.runCommand);
return parseGcloudJsonOutput(output, `Cloud Run execution ${executionName}`);
} catch (error) {
if (isGcloudResourceNotFoundError(error)) {
return null;
}
throw new Error(`Could not inspect Atlas sync run execution ${executionName}. ${getCommandErrorMessage(error).trim()}`);
}
};
export const resolveSyncRunExecutionFailureDetails = (operationResult, watchResult, dependencies = {}) => {
const executionName = normalizeOptionalString(watchResult?.executionName);
const runGcloudFileCommandImpl = dependencies.runGcloudFileCommand ?? runGcloudFileCommand;
if (!executionName || !operationResult?.jobName || !operationResult?.projectId) {
return [];
}
try {
const output = runGcloudFileCommandImpl(['logging', 'read', ['resource.type="cloud_run_job"', `resource.labels.job_name="${operationResult.jobName}"`, `labels."run.googleapis.com/execution_name"="${executionName}"`, 'severity>=ERROR'].join(' AND '), `--project=${operationResult.projectId}`, '--limit=20', '--order=desc', '--format=json'], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe']
}, dependencies.runCommand);
const parsedOutput = parseGcloudJsonOutput(output, `Cloud Run execution logs for ${executionName}`);
return extractExecutionFailureDetails(parsedOutput);
} catch {
return [];
}
};
export const watchSyncRunExecution = async (operationResult, options = {}, dependencies = {}) => {
const loggerImpl = dependencies.logger ?? logger;
const sleepImpl = dependencies.sleep ?? sleep;
const now = dependencies.now ?? (() => Date.now());
const timeoutSeconds = normalizeTimeoutSeconds(options.timeout);
const timeoutInMilliseconds = timeoutSeconds * 1000;
const pollIntervalMilliseconds = options.pollIntervalMs ?? DEFAULT_FOLLOW_POLL_INTERVAL_MS;
const getCurrentTimestamp = () => normalizeFollowTimestamp(now());
const deadline = getCurrentTimestamp() + timeoutInMilliseconds;
let executionName = null;
let hasLoggedExecutionDiscoveryWait = false;
let lastLoggedState = null;
loggerImpl.info(`Following Atlas sync execution for up to ${timeoutSeconds} seconds (job: ${operationResult.jobName}).`);
while (getCurrentTimestamp() <= deadline) {
if (!executionName) {
executionName = listLatestExecutionName(operationResult, dependencies);
if (!executionName) {
if (!hasLoggedExecutionDiscoveryWait) {
loggerImpl.info(`Waiting for Cloud Run to report the new execution for job ${operationResult.jobName}.`);
hasLoggedExecutionDiscoveryWait = true;
}
await sleepImpl(pollIntervalMilliseconds);
continue;
}
loggerImpl.info(`Following execution: ${executionName}`);
}
const execution = describeExecution(operationResult, executionName, dependencies);
if (!execution) {
await sleepImpl(pollIntervalMilliseconds);
continue;
}
const executionSummary = createExecutionSummary(execution);
if (lastLoggedState !== executionSummary.state) {
loggerImpl.summary('Execution state', [{
label: 'State',
value: executionSummary.state
}, {
label: 'Succeeded',
value: executionSummary.succeededCount
}, {
label: 'Failed',
value: executionSummary.failedCount
}]);
lastLoggedState = executionSummary.state;
}
if (executionSummary.state === 'completed') {
loggerImpl.success?.('Atlas sync run completed successfully.');
return {
executionName,
status: 'completed',
summary: executionSummary
};
}
if (executionSummary.state === 'failed') {
return {
executionName,
status: 'failed',
summary: executionSummary
};
}
await sleepImpl(pollIntervalMilliseconds);
}
return {
executionName,
status: 'timeout',
timeoutSeconds
};
};
export const runSync = async (options = {}, dependencies = {}, cwd = process.cwd()) => {
const action = normalizeRunAction(options.action);
const exit = dependencies.exit ?? (code => process.exit(code));
const loggerImpl = dependencies.logger ?? logger;
const resolveSyncRunExecutionFailureDetailsImpl = dependencies.resolveSyncRunExecutionFailureDetails ?? resolveSyncRunExecutionFailureDetails;
const runSyncBackfillOperationImpl = dependencies.runSyncBackfillOperation ?? runSyncBackfillOperation;
const runSyncIncrementalOperationImpl = dependencies.runSyncIncrementalOperation ?? runSyncIncrementalOperation;
const shouldFollowExecution = options.dryRun !== true && options.detached !== true && options.follow !== false;
const watchSyncRunExecutionImpl = dependencies.watchSyncRunExecution ?? watchSyncRunExecution;
if (!action || !SUPPORTED_RUN_ACTIONS.has(action)) {
loggerImpl.error('Atlas sync run requires <action> to be backfill or incremental.', false);
return exit(1);
}
if (action === 'incremental') {
return runSyncIncrementalOperationImpl(action, options, dependencies, cwd);
}
const operationResult = await runSyncBackfillOperationImpl(action, normalizeOptionalString(options.workloads), options, dependencies, cwd);
if (!shouldFollowExecution || operationResult?.status !== 'started') {
return operationResult;
}
let watchResult;
try {
watchResult = await watchSyncRunExecutionImpl(operationResult, options, {
...dependencies,
logger: loggerImpl
});
} catch (error) {
loggerImpl.error(error.message, false);
return exit(1);
}
if (watchResult.status === 'completed') {
return {
...operationResult,
watch: watchResult
};
}
if (watchResult.status === 'timeout') {
loggerImpl.error(`Atlas sync run timed out after ${watchResult.timeoutSeconds} seconds while waiting for the execution to complete.`, false);
} else {
loggerImpl.error(`Atlas sync run execution failed${watchResult.summary?.reason ? `: ${watchResult.summary.reason}` : '.'}`, false);
const failureDetails = resolveSyncRunExecutionFailureDetailsImpl(operationResult, watchResult, dependencies);
if (failureDetails.length > 0) {
loggerImpl.summary('Recent execution error details', failureDetails.map((detail, index) => ({
label: `Detail ${index + 1}`,
value: detail
})));
}
}
return exit(1);
};
export default async (action, options = {}) => runSync({
...(options ?? {}),
action
});