@puls-atlas/cli
Version:
The Puls Atlas CLI tool for managing Atlas projects
142 lines • 5.61 kB
JavaScript
import fs from 'fs';
import path from 'path';
import { runGcloudFileCommand } from '../../utils/gcloud.js';
import { writeJsonFile } from '../../utils/file.js';
import { getAtlasGeneratedArtifactsDir, resolveRootPath } from '../../utils/atlas.js';
import { normalizeOptionalString } from '../../utils/value.js';
import { bundleSearchMapperEntry } from './mapperBundler.js';
import { resolveSearchCloudRunDeployConfig } from './planning.js';
import { resolveSearchReleaseTarget } from './release.js';
const SEARCH_MAPPER_ARTIFACT_VERSION = 1;
const normalizePathSeparators = value => value.replaceAll('\\', '/');
const ensureDirectory = directoryPath => {
if (!fs.existsSync(directoryPath)) {
fs.mkdirSync(directoryPath, {
recursive: true
});
}
};
const normalizeGsUri = value => {
const normalized = normalizeOptionalString(value);
if (!normalized) {
return null;
}
if (!normalized.startsWith('gs://')) {
throw new Error('Atlas search mapper upload currently supports only gs:// manifest destinations.');
}
const withoutTrailingSlash = normalized.replace(/\/+$/, '');
const pathPart = withoutTrailingSlash.slice('gs://'.length);
const firstSlashIndex = pathPart.indexOf('/');
if (firstSlashIndex <= 0 || firstSlashIndex === pathPart.length - 1) {
throw new Error('Atlas search mapper manifest destination must include both a bucket and an object path.');
}
return withoutTrailingSlash;
};
const getGsUriDirectory = gsUri => gsUri.slice(0, gsUri.lastIndexOf('/'));
const joinGsUri = (baseUri, relativePath) => `${baseUri}/${normalizePathSeparators(relativePath)}`;
const getMapperArtifactRoot = (projectId, releaseId, cwd = process.cwd()) => {
const artifactRoot = path.join(getAtlasGeneratedArtifactsDir(cwd), 'search', 'mappers', projectId);
if (!releaseId) {
return artifactRoot;
}
return path.join(artifactRoot, releaseId);
};
const createUploadOperation = (localPath, remoteUri) => ({
command: 'gcloud storage cp',
localPath,
remoteUri
});
export const buildSearchMapperArtifact = async (context, options = {}, cwd = process.cwd()) => {
const releaseTarget = resolveSearchReleaseTarget(context.config?.release);
const destinationUri = normalizeGsUri(options.destinationUri ?? resolveSearchCloudRunDeployConfig(context).mapperManifestUri);
if (!destinationUri) {
throw new Error('Atlas search mapper upload requires a gs:// manifest destination. ' + 'Set search.deploy.cloudRun.mapperManifestUri or provide --destination-uri.');
}
const mapperEntries = Object.entries(context.config.mappers ?? {});
if (mapperEntries.length === 0) {
throw new Error('Atlas search mapper upload requires at least one configured mapper.');
}
const mapperSourceEntries = mapperEntries.map(([mapperName, mapperConfig]) => {
if (!mapperConfig?.source || !mapperConfig?.export) {
throw new Error(`Atlas search mapper ${mapperName} must define both source and export before it can be uploaded.`);
}
const sourcePath = normalizePathSeparators(mapperConfig.source);
const sourceFilePath = resolveRootPath(sourcePath, cwd);
if (!fs.existsSync(sourceFilePath)) {
throw new Error(`Atlas search mapper source file does not exist: ${sourceFilePath}`);
}
return {
exportName: mapperConfig.export,
mapperName,
sourceFilePath,
sourcePath
};
});
const artifactRoot = getMapperArtifactRoot(context.projectId, releaseTarget.releaseId, cwd);
const filesRoot = path.join(artifactRoot, 'files');
const manifestFilePath = path.join(artifactRoot, 'manifest.json');
const remoteBaseUri = getGsUriDirectory(destinationUri);
fs.rmSync(artifactRoot, {
force: true,
recursive: true
});
ensureDirectory(filesRoot);
const bundledEntriesBySourcePath = new Map();
const mappers = {};
const uploadOperations = [];
for (const mapperEntry of mapperSourceEntries) {
if (bundledEntriesBySourcePath.has(mapperEntry.sourceFilePath)) {
continue;
}
const bundledEntry = await bundleSearchMapperEntry({
artifactRoot,
cwd,
mapperName: mapperEntry.mapperName,
sourceFilePath: mapperEntry.sourceFilePath
});
bundledEntriesBySourcePath.set(mapperEntry.sourceFilePath, bundledEntry);
uploadOperations.push(createUploadOperation(bundledEntry.localModulePath, joinGsUri(remoteBaseUri, bundledEntry.modulePath)));
}
for (const mapperEntry of mapperSourceEntries) {
const {
modulePath
} = bundledEntriesBySourcePath.get(mapperEntry.sourceFilePath);
const moduleUri = joinGsUri(remoteBaseUri, modulePath);
mappers[mapperEntry.mapperName] = {
export: mapperEntry.exportName,
modulePath,
moduleUri,
source: mapperEntry.sourcePath
};
}
const manifest = {
mappers,
createdAt: new Date().toISOString(),
manifestUri: destinationUri,
projectId: context.projectId,
version: SEARCH_MAPPER_ARTIFACT_VERSION,
release: {
releaseId: releaseTarget.releaseId,
strategy: releaseTarget.strategy,
target: releaseTarget.target
}
};
writeJsonFile(manifestFilePath, manifest);
uploadOperations.push(createUploadOperation(manifestFilePath, destinationUri));
return {
artifactRoot,
destinationUri,
filesRoot,
manifest,
manifestFilePath,
uploadOperations
};
};
export const executeSearchMapperUpload = artifact => {
for (const operation of artifact.uploadOperations) {
runGcloudFileCommand(['storage', 'cp', operation.localPath, operation.remoteUri], {
stdio: 'inherit'
});
}
return artifact;
};