UNPKG

@puls-atlas/cli

Version:

The Puls Atlas CLI tool for managing Atlas projects

155 lines 7.06 kB
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);