@puls-atlas/cli
Version:
The Puls Atlas CLI tool for managing Atlas projects
155 lines • 7.06 kB
JavaScript
import * as features from '../../utils/feature.js';
import { loadAtlasFeatureCache, logger } from '../../utils/index.js';
import { normalizeOptionalString } from '../../utils/value.js';
import { discoverSearchRemoteResources } from './discovery.js';
import { getSearchProvider } from './providers/index.js';
import { getSearchStatusViaSearchApi } from './searchApiAdmin.js';
import { createSearchFailureHandlingSummary, createSearchRemoteSummaryEntry, createSearchRemoteSummary, logLimitedSearchRemoteDiscoveryNotice } from './planning.js';
import { createSearchSchemaLifecycleEntries, createSearchSchemaLifecycleGuidanceEntries, createSearchSchemaLifecycleSummaryRows, formatSearchSchemaLifecycleStatus } from './schemaLifecycle.js';
import { createSearchSyncFailureInspectionDetailEntries, createSearchSyncFailureInspectionSummaryRows } from './syncFailureInspection.js';
const REMOTE_DISCOVERY_LIMITED_NOTICE = 'Remote discovery is currently limited from this machine, so unavailable checks are shown as overview items instead of warnings.';
const formatSearchIndexStatusValue = value => {
const normalizedValue = normalizeOptionalString(value);
if (!normalizedValue) {
return 'unknown';
}
switch (normalizedValue) {
case 'cutover-pending':
return 'cutover pending';
case 'not-applicable':
return 'not applicable';
case 'not-inspected':
return 'not inspected';
case 'not-started':
return 'not started';
case 'idle':
return 'idle';
case 'running':
return 'running';
case 'failed':
return 'failed';
case 'mixed':
return 'mixed';
default:
return formatSearchSchemaLifecycleStatus(normalizedValue).replace(/_/g, ' ');
}
};
const createSearchIndexStatusRows = indexes => indexes.map(indexStatus => ({
label: indexStatus.indexName,
value: `${formatSearchIndexStatusValue(indexStatus.status)} | ` + `schema=${formatSearchIndexStatusValue(indexStatus.schemaStatus)} | ` + `routing=${formatSearchIndexStatusValue(indexStatus.routingStatus)} | ` + `sync=${formatSearchIndexStatusValue(indexStatus.syncFailureStatus)} | ` + `source=${formatSearchIndexStatusValue(indexStatus.sourceStatus)}`
}));
const createSearchStatusCacheStateRow = hasCache => ({
label: 'Local cache',
tone: hasCache ? 'success' : 'warning',
value: hasCache ? 'present.' : 'not present yet.'
});
const logSearchStatusOverview = (context, cacheArtifact, remoteSummary, cacheStateRow, provider, sharedStatus, loggerImpl = logger) => {
loggerImpl.summary('Status summary', [{
label: 'Project',
value: context.projectId
}, context.environment ? {
label: 'Environment',
value: context.environment
} : null, {
label: 'Provider',
value: context.config.provider
}, {
label: 'Collections',
value: Object.keys(context.config.collections).length
}, {
label: 'Indexes',
value: Object.keys(context.config.indexes).length
}, cacheStateRow, ...remoteSummary.filter(resourceGroup => resourceGroup.total > 0).map(resourceGroup => createSearchRemoteSummaryEntry(resourceGroup, {
successVerb: 'discoverable'
}))]);
loggerImpl.summary('Local files', [{
label: 'Source config',
value: context.configPath
}, {
label: 'Expected cache file',
value: cacheArtifact.filePath,
tone: 'muted'
}]);
loggerImpl.summary('Schema lifecycle', createSearchSchemaLifecycleSummaryRows(provider, sharedStatus.schemaInspection?.collections ?? [], {
includeAffectedIndexNames: true
}));
loggerImpl.summary('Index status', createSearchIndexStatusRows(sharedStatus.indexes));
loggerImpl.summary('Failure handling', createSearchFailureHandlingSummary(context));
loggerImpl.summary('Failure state', createSearchSyncFailureInspectionSummaryRows(sharedStatus.syncFailures));
const lifecycleEntries = createSearchSchemaLifecycleEntries(provider, sharedStatus.schemaInspection?.collections ?? [], {
compact: true,
includeMismatchPreview: false,
omittedStatuses: ['reindex-required']
});
if (lifecycleEntries.length > 0) {
loggerImpl.summary('Schema lifecycle details', lifecycleEntries);
}
const guidanceEntries = createSearchSchemaLifecycleGuidanceEntries(sharedStatus.schemaInspection?.collections ?? [], {
context
});
if (guidanceEntries.length > 0) {
loggerImpl.summary('Operator guidance', guidanceEntries);
}
const failureEntries = createSearchSyncFailureInspectionDetailEntries(sharedStatus.syncFailures);
if (failureEntries.length > 0) {
loggerImpl.section('Recent failed sync items', failureEntries, {
detailOnly: true
});
}
for (const warning of [...new Set(sharedStatus.warnings)]) {
loggerImpl.warning(warning);
}
};
export const reportSearchStatus = async (options, {
discoverSearchRemoteResourcesImpl = discoverSearchRemoteResources,
getSearchProviderImpl = getSearchProvider,
getSearchStatusViaSearchApiImpl = getSearchStatusViaSearchApi,
loadAtlasFeatureCacheImpl = loadAtlasFeatureCache,
loadFeatureContextImpl = features.loadFeatureContext,
loggerImpl = logger,
workingDirectory = process.cwd(),
writeOutputImpl = value => process.stdout.write(value)
} = {}) => {
const jsonOutput = options?.json === true;
let spinner;
try {
const context = await loadFeatureContextImpl('search', options, {
cwd: workingDirectory
});
if (!jsonOutput) {
spinner = loggerImpl.spinner('Loading Atlas search status...');
}
const cacheArtifact = loadAtlasFeatureCacheImpl('search', context.projectId, workingDirectory);
const remoteResources = await discoverSearchRemoteResourcesImpl(context);
const remoteSummary = createSearchRemoteSummary(context, remoteResources);
const provider = getSearchProviderImpl(context.config.provider);
const sharedStatus = await getSearchStatusViaSearchApiImpl(context);
const hasCache = Boolean(cacheArtifact.cache);
if (jsonOutput) {
writeOutputImpl(`${JSON.stringify(sharedStatus, null, 4)}\n`);
return sharedStatus;
}
spinner.succeed('Atlas search status loaded.');
logSearchStatusOverview(context, cacheArtifact, remoteSummary, createSearchStatusCacheStateRow(hasCache), provider, sharedStatus, loggerImpl);
if (!hasCache) {
loggerImpl.info('Run "atlas search apply --dry-run" if you want to refresh the local cache for this project.');
logLimitedSearchRemoteDiscoveryNotice(remoteSummary, loggerImpl, REMOTE_DISCOVERY_LIMITED_NOTICE);
return sharedStatus;
}
loggerImpl.summary('Cache metadata', [{
label: 'Cache version',
value: cacheArtifact.cache.version
}, {
label: 'Cached at',
value: cacheArtifact.cache.cachedAt
}], {
detailOnly: true
});
logLimitedSearchRemoteDiscoveryNotice(remoteSummary, loggerImpl, REMOTE_DISCOVERY_LIMITED_NOTICE);
return sharedStatus;
} catch (error) {
spinner?.fail('Failed to load Atlas search status.');
loggerImpl.error(error.message);
}
};
export default async options => reportSearchStatus(options);