@puls-atlas/cli
Version:
The Puls Atlas CLI tool for managing Atlas projects
164 lines • 8.27 kB
JavaScript
import * as features from '../../utils/feature.js';
import { logger } from '../../utils/index.js';
import { getSearchProvider } from './providers/index.js';
import { discoverSearchRemoteResources } from './discovery.js';
import { inspectSearchMapperManifest } from './manifestValidation.js';
import { createSearchFailureHandlingSummary, createSearchRemoteSummaryEntry, createSearchRemoteSummary, getSearchResourceGroups, logLimitedSearchRemoteDiscoveryNotice } from './planning.js';
import { collectSearchSchemaLifecycleVerificationIssues, createSearchSchemaLifecycleEntries, createSearchSchemaLifecycleGuidanceEntries, createSearchSchemaLifecycleSummaryRows, inspectSearchSchemasWithFallback, logSearchSchemaInspectionWarnings } from './schemaLifecycle.js';
import { collectSearchSyncFailureInspectionIssues, createSearchSyncFailureInspectionDetailEntries, createSearchSyncFailureInspectionSummaryRows, inspectSearchSyncFailures } from './syncFailureInspection.js';
const VERIFY_REMOTE_DISCOVERY_LIMITED_NOTICE = 'Remote discovery is currently limited from this machine, so unavailable checks are reported as verification gaps instead of raw warnings.';
const logSchemaMismatchDetails = (provider, schemaInspection, loggerImpl = logger) => {
const mismatchDetails = schemaInspection.collections.filter(collection => collection.schemaStatus === 'mismatch').flatMap(collection => (collection.mismatchDetails ?? []).map(detail => `${collection.name}: ${detail}`));
if (mismatchDetails.length === 0) {
return;
}
loggerImpl.section(`${provider.descriptor.labels.targetGroup} mismatch details`, mismatchDetails, {
detailOnly: true
});
};
const collectVerificationIssues = (context, provider, remoteResources, schemaInspection, manifestInspection, syncFailureInspection) => {
const issues = [];
for (const group of getSearchResourceGroups(context).filter(resourceGroup => (remoteResources[resourceGroup.key] ?? []).length > 0)) {
for (const resource of remoteResources[group.key]) {
if (resource.remoteStatus === 'found') {
continue;
}
if (resource.remoteStatus === 'not-found') {
issues.push(`${group.label}: required resource "${resource.name}" was not found.`);
continue;
}
issues.push(`${group.label}: required resource "${resource.name}" could not be verified.`);
}
}
issues.push(...collectSearchSchemaLifecycleVerificationIssues(provider, schemaInspection.collections, {
compact: true,
includeMismatchPreview: false
}));
issues.push(...manifestInspection.issues);
issues.push(...collectSearchSyncFailureInspectionIssues(syncFailureInspection));
return issues;
};
const logSearchVerificationSummary = (context, remoteSummary, provider, schemaInspection, manifestInspection, syncFailureInspection, loggerImpl = logger) => {
loggerImpl.summary('Verification summary', [{
label: 'Project',
value: context.projectId
}, context.environment ? {
label: 'Environment',
value: context.environment
} : null, ...remoteSummary.filter(resourceGroup => resourceGroup.total > 0).map(resourceGroup => createSearchRemoteSummaryEntry(resourceGroup, {
successVerb: 'verified'
}))]);
loggerImpl.summary('Local files', [{
label: 'Config',
value: context.configPath
}]);
loggerImpl.summary('Failure handling', createSearchFailureHandlingSummary(context));
loggerImpl.summary('Failure state', createSearchSyncFailureInspectionSummaryRows(syncFailureInspection));
logSchemaVerificationSummary(provider, schemaInspection, loggerImpl);
loggerImpl.summary('Schema lifecycle', createSearchSchemaLifecycleSummaryRows(provider, schemaInspection.collections ?? [], {
includeAffectedIndexNames: true
}));
loggerImpl.summary('Mapper manifest', [{
label: 'Status',
tone: manifestInspection.status === 'ok' ? 'success' : manifestInspection.status === 'invalid' ? 'error' : 'warning',
value: manifestInspection.status
}, {
label: 'URI',
value: manifestInspection.manifestUri ?? 'not configured'
}]);
logSchemaMismatchDetails(provider, schemaInspection, loggerImpl);
const lifecycleEntries = createSearchSchemaLifecycleEntries(provider, schemaInspection.collections ?? [], {
compact: true,
includeMismatchPreview: false,
omittedStatuses: ['reindex-required']
});
if (lifecycleEntries.length > 0) {
loggerImpl.summary('Schema lifecycle details', lifecycleEntries);
}
const guidanceEntries = createSearchSchemaLifecycleGuidanceEntries(schemaInspection.collections ?? [], {
context
});
if (guidanceEntries.length > 0) {
loggerImpl.summary('Operator guidance', guidanceEntries);
}
const failureEntries = createSearchSyncFailureInspectionDetailEntries(syncFailureInspection);
if (failureEntries.length > 0) {
loggerImpl.section('Recent failed sync items', failureEntries, {
detailOnly: true
});
}
};
const logSchemaVerificationSummary = (provider, schemaInspection, loggerImpl = logger) => {
const totalCount = schemaInspection.collections.length;
if (totalCount === 0) {
return;
}
const unknownCount = schemaInspection.collections.filter(collection => collection.schemaStatus === 'unknown').length;
if (unknownCount > 0) {
loggerImpl.summary('Schema status', [{
label: provider.descriptor.labels.targetGroup,
tone: 'warning',
value: 'could not be verified from this machine.'
}]);
return;
}
const verifiedCount = schemaInspection.collections.filter(collection => collection.schemaStatus === 'match').length;
loggerImpl.summary('Schema status', [{
label: provider.descriptor.labels.targetGroup,
tone: verifiedCount === totalCount ? 'success' : 'warning',
value: `${verifiedCount}/${totalCount} verified.`
}]);
};
export const verifySearch = async (options, {
discoverSearchRemoteResourcesImpl = discoverSearchRemoteResources,
exitImpl = code => process.exit(code),
getSearchProviderImpl = getSearchProvider,
inspectSearchSchemasDirectImpl = null,
inspectSearchSchemasImpl,
inspectSearchSyncFailuresImpl = inspectSearchSyncFailures,
inspectSearchMapperManifestImpl = inspectSearchMapperManifest,
loadFeatureContextImpl = features.loadFeatureContext,
loggerImpl = logger,
workingDirectory = process.cwd()
} = {}) => {
let spinner;
try {
const context = await loadFeatureContextImpl('search', options, {
cwd: workingDirectory
});
spinner = loggerImpl.spinner('Verifying Atlas search...');
const provider = getSearchProviderImpl(context.config.provider);
const remoteResources = await discoverSearchRemoteResourcesImpl(context);
const remoteSummary = createSearchRemoteSummary(context, remoteResources);
const schemaInspection = await inspectSearchSchemasWithFallback(context, {
getSearchProviderImpl,
inspectSearchSchemasDirectImpl,
inspectSearchSchemasViaSearchApiImpl: inspectSearchSchemasImpl,
provider
});
logSearchSchemaInspectionWarnings(schemaInspection, loggerImpl);
const syncFailureInspection = await inspectSearchSyncFailuresImpl(context);
if (syncFailureInspection.warning) {
loggerImpl.warning(syncFailureInspection.warning);
}
const manifestInspection = await inspectSearchMapperManifestImpl(context);
const issues = collectVerificationIssues(context, provider, remoteResources, schemaInspection, manifestInspection, syncFailureInspection);
if (issues.length > 0) {
spinner.fail('Atlas search verification failed.');
logSearchVerificationSummary(context, remoteSummary, provider, schemaInspection, manifestInspection, syncFailureInspection, loggerImpl);
logLimitedSearchRemoteDiscoveryNotice(remoteSummary, loggerImpl, VERIFY_REMOTE_DISCOVERY_LIMITED_NOTICE);
for (const issue of issues) {
loggerImpl.error(issue, false);
}
exitImpl(1);
return;
}
spinner.succeed('Atlas search verification passed.');
logSearchVerificationSummary(context, remoteSummary, provider, schemaInspection, manifestInspection, syncFailureInspection, loggerImpl);
} catch (error) {
spinner?.fail('Atlas search verification failed.');
loggerImpl.error(error.message, false);
exitImpl(1);
}
};
export default verifySearch;