@puls-atlas/cli
Version:
The Puls Atlas CLI tool for managing Atlas projects
150 lines • 6.62 kB
JavaScript
import { execFileSync as nodeExecFileSync } from 'child_process';
import { isPlainObject } from 'es-toolkit/compat';
import { resolveSearchMapperManifestUri } from './artifactBucket.js';
import { DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS, fetchWithTimeout } from './providers/request.js';
import { runGcloudFileCommand } from '../../utils/gcloud.js';
import { normalizeOptionalString } from '../../utils/value.js';
export const SEARCH_MAPPER_MANIFEST_VERSION = 1;
const isSupportedManifestUri = value => {
const normalized = normalizeOptionalString(value);
return normalized !== null && (normalized.startsWith('gs://') || normalized.startsWith('https://'));
};
const readGsManifest = (manifestUri, runCommand = nodeExecFileSync) => runGcloudFileCommand(['storage', 'cat', manifestUri], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe']
}, runCommand);
const readHttpManifest = async (manifestUri, options = {}) => {
const {
fetchImpl = fetch,
timeoutMs = DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS
} = options;
const response = await fetchWithTimeout(manifestUri, {}, timeoutMs, {
fetchImpl
});
if (!response.ok) {
throw new Error(`Atlas search mapper manifest fetch failed with status ${response.status}.`);
}
return response.text();
};
const readManifestText = async (manifestUri, options = {}) => {
const {
fetchImpl = fetch,
runCommand = nodeExecFileSync,
timeoutMs = DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS
} = options;
if (!isSupportedManifestUri(manifestUri)) {
throw new Error('Atlas search mapper manifest validation supports only gs:// and https:// destinations.');
}
if (manifestUri.startsWith('gs://')) {
return readGsManifest(manifestUri, runCommand);
}
return readHttpManifest(manifestUri, {
fetchImpl,
timeoutMs
});
};
const isMissingManifestError = error => {
const message = `${error?.stderr?.toString?.() ?? ''}\n${error?.message ?? ''}`;
return /404|not[ -]?found|no urls matched|matched no objects or files|bucket.+not found|object.+not found|status 404/i.test(message);
};
const getReadableManifestReadError = error => {
const stderr = error?.stderr?.toString?.().trim();
if (stderr) {
return stderr;
}
return String(error?.message ?? 'Unknown error').replace(/^Command failed:[\s\S]*?\r?\n/, '').trim();
};
const formatManifestReadIssue = (manifestUri, error) => {
if (isMissingManifestError(error)) {
return `Atlas search mapper manifest is missing at ${manifestUri}.`;
}
return `Could not read Atlas search mapper manifest from ${manifestUri}. ${getReadableManifestReadError(error)}`.trim();
};
const parseManifestJson = manifestText => {
try {
return JSON.parse(manifestText);
} catch (error) {
throw new Error(`Atlas search mapper manifest contains invalid JSON. ${error.message}`);
}
};
export const validateSearchMapperManifest = (context, manifest, manifestUri) => {
const issues = [];
const warnings = [];
if (!isPlainObject(manifest)) {
return {
issues: ['Atlas search mapper manifest must resolve to a JSON object.'],
warnings
};
}
if (manifest.version !== SEARCH_MAPPER_MANIFEST_VERSION) {
issues.push(`Atlas search mapper manifest version ${manifest.version ?? 'unknown'} is not supported. ` + `Expected ${SEARCH_MAPPER_MANIFEST_VERSION}.`);
}
if (!manifest.mappers || !isPlainObject(manifest.mappers)) {
issues.push('Atlas search mapper manifest must define a "mappers" object.');
return {
issues,
warnings
};
}
if (manifest.projectId && manifest.projectId !== context.projectId) {
issues.push(`Atlas search mapper manifest projectId ${manifest.projectId} does not match the selected project ${context.projectId}.`);
}
if (manifest.manifestUri && manifest.manifestUri !== manifestUri) {
warnings.push(`Atlas search mapper manifest declares manifestUri ${manifest.manifestUri}, ` + `but the active Atlas search config points to ${manifestUri}.`);
}
if (manifest.manifestUri && !isSupportedManifestUri(manifest.manifestUri)) {
issues.push(`Atlas search mapper manifest declares an unsupported manifestUri ${manifest.manifestUri}. ` + 'Only gs:// and https:// URIs are allowed.');
}
for (const [mapperName, mapperConfig] of Object.entries(context.config.mappers ?? {})) {
const manifestMapper = manifest.mappers[mapperName] ?? null;
if (!manifestMapper) {
issues.push(`Atlas search mapper manifest is missing mapper ${mapperName} required by the current search config.`);
continue;
}
if (manifestMapper.export !== mapperConfig.export) {
issues.push(`Atlas search mapper manifest export mismatch for ${mapperName}. ` + `Expected ${mapperConfig.export}, received ${manifestMapper.export ?? 'undefined'}.`);
}
if (manifestMapper.source !== mapperConfig.source) {
issues.push(`Atlas search mapper manifest source mismatch for ${mapperName}. ` + `Expected ${mapperConfig.source}, received ${manifestMapper.source ?? 'undefined'}.`);
}
if (!normalizeOptionalString(manifestMapper.modulePath)) {
issues.push(`Atlas search mapper manifest entry ${mapperName} must define a non-empty modulePath.`);
}
if (!normalizeOptionalString(manifestMapper.moduleUri)) {
issues.push(`Atlas search mapper manifest entry ${mapperName} must define a non-empty moduleUri.`);
} else if (!isSupportedManifestUri(manifestMapper.moduleUri)) {
issues.push(`Atlas search mapper manifest entry ${mapperName} must use a gs:// or https:// moduleUri.`);
}
}
for (const manifestMapperName of Object.keys(manifest.mappers)) {
if (!context.config.mappers?.[manifestMapperName]) {
warnings.push(`Atlas search mapper manifest contains extra mapper ${manifestMapperName} ` + 'that is not referenced by the current search config.');
}
}
return {
issues,
warnings
};
};
export const inspectSearchMapperManifest = async (context, options = {}) => {
const manifestUri = resolveSearchMapperManifestUri(context);
try {
const manifestText = await readManifestText(manifestUri, options);
const manifest = parseManifestJson(manifestText);
const validation = validateSearchMapperManifest(context, manifest, manifestUri);
return {
...validation,
manifest,
manifestUri,
status: validation.issues.length > 0 ? 'invalid' : 'ok'
};
} catch (error) {
return {
issues: [formatManifestReadIssue(manifestUri, error)],
manifest: null,
manifestUri,
status: isMissingManifestError(error) ? 'missing' : 'unreachable',
warnings: []
};
}
};