@puls-atlas/cli
Version:
The Puls Atlas CLI tool for managing Atlas projects
635 lines • 26.5 kB
JavaScript
import path from 'path';
import { isString } from 'es-toolkit/compat';
import { cloneDeep as clone, merge, omit } from 'es-toolkit/object';
import { isEqual, isPlainObject } from 'es-toolkit/predicate';
import { readJsonFile, writeJsonFile } from '../../../utils/file.js';
import { normalizeOptionalString } from '../../../utils/value.js';
import { resolveAtlasWorkloadFile } from '../../../utils/atlas.js';
import { getSearchProvider } from '../providers/index.js';
import { resolveProjectScopedSearchSourceConfig } from './sources.js';
import { getProviderSectionEntries, validateSearchProjectSection, validateResolvedSearchConfig, validateSearchWorkloadConfig, validateSearchRootSection } from './searchValidation.js';
const SEARCH_WORKLOAD_NAME = 'search';
const SEARCH_DIRECTORY_PREFIX = 'search/';
const DEFAULT_SEARCH_PROVIDER = 'typesense';
const SEARCH_CONFIG_FILE_NAME = 'config.json';
const SEARCH_WORKLOAD_DIRECTORY_PREFIX = 'services/search/';
export const DEFAULT_SEARCH_WORKLOAD_PATH = 'workloads/users.json';
export const DEFAULT_SEARCH_TERRAFORM_ROOT_DIR = 'services/search/terraform';
export const DEFAULT_SEARCH_WORKLOAD_TERRAFORM_ROOT_DIR = DEFAULT_SEARCH_TERRAFORM_ROOT_DIR;
export const DEFAULT_SEARCH_TERRAFORM_MODULE_RELEASE_TAG = 'v0.1.0';
export const DEFAULT_SEARCH_TERRAFORM_MODULE_SOURCE = `git::https://github.com/limebooth/atlas-terraform-modules.git//search/backfill_job?ref=${DEFAULT_SEARCH_TERRAFORM_MODULE_RELEASE_TAG}`;
export const DEFAULT_SEARCH_SYNC_FAILURE_COLLECTION_PATH = 'sync/search/sync_state';
export const DEFAULT_SEARCH_SYNC_FAILURE_RECENT_LIMIT = 5;
export const DEFAULT_ATLAS_SEARCH_WORKLOAD_CONTENT = {
source: {
enabled: false,
firestore: {
documentPathPattern: 'users/*'
},
name: 'users_firestore',
routeTemplate: '/users/:id',
syncClass: 'delta-merge',
type: 'firestore'
},
index: {
fields: [{
name: 'id',
type: 'string'
}, {
name: 'entityType',
type: 'string'
}, {
name: 'title',
type: 'string'
}, {
name: 'content',
type: 'string'
}, {
name: 'displayName',
type: 'string'
}, {
name: 'email',
type: 'string'
}, {
name: 'fname',
type: 'string'
}, {
name: 'lname',
type: 'string'
}, {
facet: true,
name: 'role',
optional: true,
type: 'string[]'
}, {
facet: true,
filter: true,
name: 'status',
optional: true,
type: 'int32'
}, {
name: 'phoneNumber',
optional: true,
type: 'string'
}, {
name: 'photoURL',
optional: true,
type: 'string'
}, {
name: 'sourcePath',
type: 'string'
}, {
group: true,
name: 'organizationId',
optional: true,
type: 'string'
}, {
name: 'updatedAt',
optional: true,
sort: true,
type: 'int64'
}],
defaultSort: [{
direction: 'desc',
field: 'updatedAt'
}],
facets: ['role', 'status'],
filterBy: ['status'],
groupBy: ['organizationId'],
name: 'users_search',
queryBy: ['displayName', 'email', 'fname', 'lname']
},
mapper: {
export: 'mapUserToSearchDocument',
name: 'user',
source: 'mappers/user.ts'
}
};
const getSearchProjectEntries = searchConfig => isPlainObject(searchConfig?.projects) ? Object.entries(searchConfig.projects) : [];
export const hasSearchProjectConfigs = searchConfig => getSearchProjectEntries(searchConfig).length > 0;
export const createDefaultSearchCloudRunServiceAccountEmail = projectId => {
const normalizedProjectId = normalizeOptionalString(projectId);
if (!normalizedProjectId) {
return null;
}
return `${normalizedProjectId}@appspot.gserviceaccount.com`;
};
export const resolveSearchCloudRunServiceAccountEmail = (searchConfig, projectId) => {
const configuredValue = searchConfig?.deploy?.cloudRun?.serviceAccountEmail;
return normalizeOptionalString(configuredValue) ?? createDefaultSearchCloudRunServiceAccountEmail(projectId);
};
const normalizePathSeparators = filePath => filePath.replaceAll(path.sep, '/');
export const resolveSearchConfigLocation = (cwd = process.cwd(), dependencies = {}) => {
const resolution = resolveAtlasWorkloadFile(SEARCH_WORKLOAD_NAME, SEARCH_CONFIG_FILE_NAME, cwd, dependencies);
return {
...resolution,
configDirectory: path.dirname(resolution.activePath),
configPath: resolution.activePath
};
};
const resolveSearchScopedPath = (targetPath, cwd, baseDirectory) => {
const normalizedTargetPath = normalizePathSeparators(targetPath);
if (baseDirectory !== cwd && (normalizedTargetPath.startsWith(SEARCH_DIRECTORY_PREFIX) || normalizedTargetPath.startsWith(SEARCH_WORKLOAD_DIRECTORY_PREFIX))) {
return path.resolve(cwd, normalizedTargetPath);
}
return path.resolve(baseDirectory, normalizedTargetPath);
};
const createDefaultSearchRootConfig = (providerName = DEFAULT_SEARCH_PROVIDER, options = {}) => {
const {
terraformRootDir = DEFAULT_SEARCH_TERRAFORM_ROOT_DIR
} = options;
const provider = getSearchProvider(providerName ?? DEFAULT_SEARCH_PROVIDER);
const rootConfig = {
provider: provider.name,
workloads: [DEFAULT_SEARCH_WORKLOAD_PATH],
deploy: {
cloudRun: {
artifactRegistryLocation: 'europe-west1',
artifactRegistryProject: 'puls-atlas-core',
mapperManifestUri: null,
repository: 'atlas-runtime-containers',
region: 'europe-west1',
runtimeConfigUri: null,
searchConfigTransport: 'artifact'
},
syncState: {
collectionPath: DEFAULT_SEARCH_SYNC_FAILURE_COLLECTION_PATH,
recentLimit: DEFAULT_SEARCH_SYNC_FAILURE_RECENT_LIMIT
},
terraform: {
moduleSource: DEFAULT_SEARCH_TERRAFORM_MODULE_SOURCE,
rootDir: terraformRootDir
}
},
release: {
active: null,
candidate: null,
strategy: 'direct'
}
};
if (provider.configSectionKey && provider.createDefaultConfigSection) {
rootConfig[provider.configSectionKey] = provider.createDefaultConfigSection();
}
return rootConfig;
};
const createDefaultSearchSharedRootConfig = (providerName = DEFAULT_SEARCH_PROVIDER) => ({
workloads: [DEFAULT_SEARCH_WORKLOAD_PATH],
provider: getSearchProvider(providerName ?? DEFAULT_SEARCH_PROVIDER).name
});
const createDefaultSearchProjectConfig = (providerName = DEFAULT_SEARCH_PROVIDER, options = {}) => {
const defaultSearchConfig = createDefaultSearchRootConfig(providerName, options);
const defaultProjectConfig = {
deploy: clone(defaultSearchConfig.deploy ?? {}),
release: clone(defaultSearchConfig.release ?? {})
};
for (const [sectionKey] of getProviderSectionEntries()) {
if (defaultSearchConfig[sectionKey] === undefined) {
continue;
}
defaultProjectConfig[sectionKey] = clone(defaultSearchConfig[sectionKey]);
}
return defaultProjectConfig;
};
const createCompleteSearchProjectConfig = (projectConfig, providerName, options = {}) => {
const defaultProjectConfig = createDefaultSearchProjectConfig(providerName, options);
const mergedProjectConfig = {};
const configuredEnvironment = normalizeOptionalString(projectConfig?.environment);
if (configuredEnvironment) {
mergedProjectConfig.environment = configuredEnvironment;
}
mergedProjectConfig.deploy = merge(clone(defaultProjectConfig.deploy ?? {}), projectConfig?.deploy ?? {});
mergedProjectConfig.release = merge(clone(defaultProjectConfig.release ?? {}), projectConfig?.release ?? {});
for (const [sectionKey] of getProviderSectionEntries()) {
const defaultSection = defaultProjectConfig[sectionKey];
const projectSection = projectConfig?.[sectionKey];
if (defaultSection === undefined && projectSection === undefined) {
continue;
}
mergedProjectConfig[sectionKey] = merge(clone(defaultSection ?? {}), projectSection ?? {});
}
if (projectConfig?.sourceBindings !== undefined) {
mergedProjectConfig.sourceBindings = clone(projectConfig.sourceBindings);
}
return validateSearchProjectSection(mergedProjectConfig, options.projectId);
};
const createDefaultProjectScopedSearchRootConfig = (providerName = DEFAULT_SEARCH_PROVIDER, options = {}) => ({
...createDefaultSearchSharedRootConfig(providerName),
projects: {
[options.projectId]: createDefaultSearchProjectConfig(providerName, options)
}
});
export const createDefaultSearchSection = (providerName, options = {}) => clone(createDefaultSearchRootConfig(providerName, options));
const createCompleteSearchRootConfig = (searchConfig, options = {}) => {
const defaultSearchConfig = hasSearchProjectConfigs(searchConfig) ? createDefaultSearchSharedRootConfig(searchConfig?.provider) : createDefaultSearchSection(searchConfig?.provider, options);
const providerSectionEntries = getProviderSectionEntries();
const providerSectionKeys = new Set(providerSectionEntries.map(([k]) => k));
const mergedConfig = clone(defaultSearchConfig);
for (const [key, value] of Object.entries(searchConfig ?? {})) {
if (key !== 'deploy' && key !== 'projects' && key !== 'release' && !providerSectionKeys.has(key)) {
mergedConfig[key] = clone(value);
}
}
if (defaultSearchConfig.deploy !== undefined || searchConfig?.deploy !== undefined) {
mergedConfig.deploy = merge(clone(defaultSearchConfig.deploy ?? {}), searchConfig?.deploy ?? {});
}
if (defaultSearchConfig.release !== undefined || searchConfig?.release !== undefined) {
mergedConfig.release = merge(clone(defaultSearchConfig.release ?? {}), searchConfig?.release ?? {});
}
for (const [sectionKey] of providerSectionEntries) {
if (defaultSearchConfig[sectionKey] === undefined && searchConfig?.[sectionKey] === undefined) {
continue;
}
mergedConfig[sectionKey] = merge(clone(defaultSearchConfig[sectionKey] ?? {}), searchConfig?.[sectionKey] ?? {});
}
if (searchConfig?.projects !== undefined || hasSearchProjectConfigs(searchConfig)) {
mergedConfig.projects = {};
for (const [projectId, projectConfig] of getSearchProjectEntries(searchConfig)) {
mergedConfig.projects[projectId] = createCompleteSearchProjectConfig(projectConfig, mergedConfig.provider, {
...options,
projectId
});
}
}
return validateSearchRootSection(mergedConfig);
};
export const resolveSearchProjectConfig = (searchConfig, projectId, options = {}) => {
const completeRootConfig = createCompleteSearchRootConfig(searchConfig, options);
if (!hasSearchProjectConfigs(completeRootConfig)) {
return {
config: completeRootConfig,
projectConfig: null,
rootConfig: completeRootConfig
};
}
const normalizedProjectId = normalizeOptionalString(projectId);
if (!normalizedProjectId) {
throw new Error('Atlas search config defines project-specific settings under "projects". Re-run with --project or --environment to select a project.');
}
const resolvedProjectConfig = completeRootConfig.projects?.[normalizedProjectId] ?? null;
if (!resolvedProjectConfig) {
throw new Error(`Atlas search config does not define a "projects.${normalizedProjectId}" entry.`);
}
const configuredEnvironment = normalizeOptionalString(resolvedProjectConfig.environment);
const selectedEnvironment = normalizeOptionalString(options.environment);
if (configuredEnvironment !== null && selectedEnvironment !== null && configuredEnvironment !== selectedEnvironment) {
throw new Error(`Atlas search config project "${normalizedProjectId}" declares environment "${configuredEnvironment}", but the selected project resolves to "${selectedEnvironment}" via .firebaserc.`);
}
const resolvedConfig = clone(omit(completeRootConfig, ['projects']));
resolvedConfig.deploy = merge(clone(completeRootConfig.deploy ?? {}), resolvedProjectConfig.deploy ?? {});
resolvedConfig.release = merge(clone(completeRootConfig.release ?? {}), resolvedProjectConfig.release ?? {});
for (const [sectionKey] of getProviderSectionEntries()) {
const rootSection = completeRootConfig[sectionKey];
const projectSection = resolvedProjectConfig[sectionKey];
if (rootSection === undefined && projectSection === undefined) {
continue;
}
resolvedConfig[sectionKey] = merge(clone(rootSection ?? {}), projectSection ?? {});
}
return {
config: resolvedConfig,
projectConfig: resolvedProjectConfig,
rootConfig: completeRootConfig
};
};
export const applySearchProjectConfigPatch = (searchConfig, projectId, patch, options = {}) => {
const completeRootConfig = createCompleteSearchRootConfig(searchConfig, options);
const requiresProjectPatch = hasSearchProjectConfigs(completeRootConfig) || patch.environment !== undefined || patch.sourceBindings !== undefined;
if (!requiresProjectPatch) {
const nextRootConfig = clone(completeRootConfig);
if (patch.deploy !== undefined) {
nextRootConfig.deploy = merge(clone(nextRootConfig.deploy ?? {}), patch.deploy ?? {});
}
if (patch.release !== undefined) {
nextRootConfig.release = merge(clone(nextRootConfig.release ?? {}), patch.release ?? {});
}
for (const [sectionKey] of getProviderSectionEntries()) {
if (patch[sectionKey] === undefined) {
continue;
}
nextRootConfig[sectionKey] = merge(clone(nextRootConfig[sectionKey] ?? {}), patch[sectionKey] ?? {});
}
return validateSearchRootSection(nextRootConfig);
}
const normalizedProjectId = normalizeOptionalString(projectId);
if (!normalizedProjectId) {
throw new Error('Atlas search config defines project-specific settings under "projects". Re-run with --project or --environment to select a project.');
}
const currentProjectConfig = completeRootConfig.projects?.[normalizedProjectId] ?? {};
const nextProjectConfig = clone(currentProjectConfig);
if (patch.environment !== undefined) {
const normalizedEnvironment = normalizeOptionalString(patch.environment);
if (normalizedEnvironment) {
nextProjectConfig.environment = normalizedEnvironment;
} else {
delete nextProjectConfig.environment;
}
}
if (patch.deploy !== undefined) {
nextProjectConfig.deploy = merge(clone(currentProjectConfig.deploy ?? {}), patch.deploy ?? {});
}
if (patch.release !== undefined) {
nextProjectConfig.release = merge(clone(currentProjectConfig.release ?? {}), patch.release ?? {});
}
if (patch.sourceBindings !== undefined) {
nextProjectConfig.sourceBindings = merge(clone(currentProjectConfig.sourceBindings ?? {}), patch.sourceBindings ?? {});
}
for (const [sectionKey] of getProviderSectionEntries()) {
if (patch[sectionKey] === undefined) {
continue;
}
nextProjectConfig[sectionKey] = merge(clone(currentProjectConfig[sectionKey] ?? {}), patch[sectionKey] ?? {});
}
return validateSearchRootSection({
...completeRootConfig,
projects: {
...(completeRootConfig.projects ?? {}),
[normalizedProjectId]: createCompleteSearchProjectConfig(nextProjectConfig, completeRootConfig.provider, {
...options,
projectId: normalizedProjectId
})
}
});
};
const normalizeSearchWorkloadConfig = (config, cwd, sourceBaseDirectory) => {
const normalizedConfig = clone(config);
if (isString(normalizedConfig.mapper?.source)) {
const resolvedSourcePath = resolveSearchScopedPath(normalizedConfig.mapper.source, cwd, sourceBaseDirectory);
normalizedConfig.mapper.source = normalizePathSeparators(path.relative(cwd, resolvedSourcePath));
}
return normalizedConfig;
};
const createSearchCollectionFromFirestoreWorkload = (sourceConfig, mapperConfig, indexConfig) => {
const collectionConfig = {
enabled: sourceConfig?.enabled !== false,
indexes: [indexConfig.name],
mapper: mapperConfig.name,
sourcePathPattern: sourceConfig.firestore.documentPathPattern
};
const routeTemplate = normalizeOptionalString(sourceConfig?.routeTemplate);
if (routeTemplate) {
collectionConfig.routeTemplate = routeTemplate;
}
return collectionConfig;
};
const createSearchSourceFromFirestoreWorkload = (sourceConfig, mapperConfig, indexConfig) => {
const sourceName = sourceConfig.name;
const firestoreConfig = sourceConfig.firestore ?? {};
const resolvedSourceConfig = {
collection: sourceName,
enabled: sourceConfig?.enabled !== false,
eventarc: {
documentPathPattern: firestoreConfig.documentPathPattern
},
indexes: [indexConfig.name],
mapper: mapperConfig.name,
syncClass: sourceConfig.syncClass,
type: 'firestore'
};
const description = normalizeOptionalString(sourceConfig?.description);
const routeTemplate = normalizeOptionalString(sourceConfig?.routeTemplate);
if (description) {
resolvedSourceConfig.description = description;
}
if (routeTemplate) {
resolvedSourceConfig.routeTemplate = routeTemplate;
}
if (normalizeOptionalString(firestoreConfig.database)) {
resolvedSourceConfig.eventarc.database = firestoreConfig.database;
}
if (normalizeOptionalString(firestoreConfig.triggerRegion)) {
resolvedSourceConfig.eventarc.triggerRegion = firestoreConfig.triggerRegion;
}
return resolvedSourceConfig;
};
const createSearchScheduledSourceConfigFromWorkload = (workloadConfig, sourceConfig) => ({
...clone(omit(sourceConfig, ['enabled', 'firestore', 'name'])),
indexes: [workloadConfig.index.name],
mapper: workloadConfig.mapper.name
});
const resolveProjectScopedSearchSources = (scheduledSourceConfigs, sourceBindings, projectId = null) => {
const resolvedSources = {};
for (const [sourceName, sourceBinding] of Object.entries(sourceBindings ?? {})) {
const scheduledSourceConfig = scheduledSourceConfigs?.[sourceName] ?? null;
if (!scheduledSourceConfig) {
throw new Error(`Atlas search project${projectId ? ` "${projectId}"` : ''} defines a scheduled source binding for ` + `"${sourceName}" but no matching workload-defined source exists.`);
}
resolvedSources[sourceName] = resolveProjectScopedSearchSourceConfig(sourceName, scheduledSourceConfig, sourceBinding);
}
return resolvedSources;
};
const mergeSearchRegistry = (target, source, registryName, sourceLabel) => {
for (const [entryName, entryConfig] of Object.entries(source ?? {})) {
if (Object.hasOwn(target, entryName)) {
throw new Error(`Duplicate Atlas search ${registryName} "${entryName}" found while loading ${sourceLabel}.`);
}
target[entryName] = clone(entryConfig);
}
};
const resolveSearchConfig = (searchConfig, version, cwd = process.cwd(), options = {}) => {
const {
configBaseDirectory = cwd,
projectId = null,
sourceBaseDirectory = cwd
} = options;
const defaultTerraformRootDir = options.defaultTerraformRootDir ?? DEFAULT_SEARCH_TERRAFORM_ROOT_DIR;
const {
config: validatedSearchConfig,
projectConfig
} = resolveSearchProjectConfig(searchConfig, projectId, {
environment: options.environment ?? null,
defaultTerraformRootDir,
terraformRootDir: defaultTerraformRootDir
});
const resolvedConfig = {
collections: {},
deploy: clone(validatedSearchConfig.deploy ?? {}),
indexes: {},
mappers: {},
provider: validatedSearchConfig.provider,
release: clone(validatedSearchConfig.release ?? {}),
sources: {},
version
};
const resolvedScheduledSourceConfigs = {};
for (const [sectionKey] of getProviderSectionEntries()) {
resolvedConfig[sectionKey] = clone(validatedSearchConfig[sectionKey] ?? {});
}
const resolvedConfigPaths = [];
for (const workloadRelativePath of validatedSearchConfig.workloads ?? []) {
const workloadPath = resolveSearchScopedPath(workloadRelativePath, cwd, configBaseDirectory);
const workloadConfig = normalizeSearchWorkloadConfig(validateSearchWorkloadConfig(readJsonFile(workloadPath), workloadPath), cwd, sourceBaseDirectory);
const sourceName = workloadConfig.source.name;
const mapperName = workloadConfig.mapper.name;
const indexName = workloadConfig.index.name;
mergeSearchRegistry(resolvedConfig.mappers, {
[mapperName]: omit(workloadConfig.mapper, ['name'])
}, 'mapper', workloadPath);
mergeSearchRegistry(resolvedConfig.indexes, {
[indexName]: omit(workloadConfig.index, ['name'])
}, 'index', workloadPath);
if (workloadConfig.source.type.trim().toLowerCase() === 'firestore') {
mergeSearchRegistry(resolvedConfig.collections, {
[sourceName]: createSearchCollectionFromFirestoreWorkload(workloadConfig.source, workloadConfig.mapper, workloadConfig.index)
}, 'collection', workloadPath);
mergeSearchRegistry(resolvedConfig.sources, {
[sourceName]: createSearchSourceFromFirestoreWorkload(workloadConfig.source, workloadConfig.mapper, workloadConfig.index)
}, 'source', workloadPath);
} else {
mergeSearchRegistry(resolvedScheduledSourceConfigs, {
[sourceName]: createSearchScheduledSourceConfigFromWorkload(workloadConfig, workloadConfig.source)
}, 'scheduled source config', workloadPath);
}
resolvedConfigPaths.push(workloadPath);
}
mergeSearchRegistry(resolvedConfig.sources, resolveProjectScopedSearchSources(resolvedScheduledSourceConfigs, projectConfig?.sourceBindings ?? {}, projectId), 'source', `project${projectId ? ` "${projectId}"` : ''} source bindings`);
return {
config: validateResolvedSearchConfig(resolvedConfig),
configPaths: resolvedConfigPaths,
projectConfig
};
};
export const loadSearchConfig = (cwd = process.cwd(), options = {}) => {
const version = 1;
const configLocation = resolveSearchConfigLocation(cwd);
const searchConfig = readJsonFile(configLocation.configPath, {
allowMissing: true
});
const {
configPath,
configDirectory,
preferredPath
} = configLocation;
if (searchConfig) {
const {
config,
configPaths,
projectConfig
} = resolveSearchConfig(searchConfig, version, cwd, {
configBaseDirectory: configDirectory,
defaultTerraformRootDir: DEFAULT_SEARCH_WORKLOAD_TERRAFORM_ROOT_DIR,
environment: options.environment ?? null,
projectId: options.projectId ?? null,
sourceBaseDirectory: configDirectory
});
return {
config,
configPath,
configPaths,
projectConfig,
rootConfig: searchConfig,
warnings: []
};
}
throw new Error(`Atlas search config is missing. Expected ${preferredPath}.`);
};
export const ensureSearchConfigSection = (cwd = process.cwd(), options = {}) => {
const createdFiles = [];
const updatedFiles = [];
const configLocation = resolveSearchConfigLocation(cwd);
const {
configPath
} = configLocation;
const existingSearchConfig = readJsonFile(configPath, {
allowMissing: true
});
const didSearchConfigExist = Boolean(existingSearchConfig);
const environment = normalizeOptionalString(options.environment);
const providerName = normalizeOptionalString(options.providerName)?.toLowerCase();
const projectId = normalizeOptionalString(options.projectId);
const defaultTerraformRootDir = DEFAULT_SEARCH_WORKLOAD_TERRAFORM_ROOT_DIR;
let nextSearchConfigInput = existingSearchConfig;
if (!nextSearchConfigInput) {
nextSearchConfigInput = projectId ? createDefaultProjectScopedSearchRootConfig(providerName, {
environment,
projectId,
terraformRootDir: defaultTerraformRootDir
}) : createDefaultSearchSection(providerName, {
terraformRootDir: defaultTerraformRootDir
});
} else if (!hasSearchProjectConfigs(nextSearchConfigInput) && projectId) {
nextSearchConfigInput = {
...clone(nextSearchConfigInput),
projects: {
[projectId]: {}
}
};
} else if (hasSearchProjectConfigs(nextSearchConfigInput) && projectId) {
nextSearchConfigInput = {
...clone(nextSearchConfigInput),
projects: {
...(nextSearchConfigInput.projects ?? {}),
[projectId]: {
...(nextSearchConfigInput.projects?.[projectId] ?? {})
}
}
};
} else if (hasSearchProjectConfigs(nextSearchConfigInput) && !projectId) {
throw new Error('Atlas search config defines project-specific settings under "projects". Re-run with --project or --environment to select a project.');
}
const nextSearchConfig = createCompleteSearchRootConfig(nextSearchConfigInput, {
terraformRootDir: defaultTerraformRootDir
});
writeJsonFile(configPath, nextSearchConfig);
if (!didSearchConfigExist) {
createdFiles.push(configPath);
} else if (!isEqual(existingSearchConfig, nextSearchConfig)) {
updatedFiles.push(configPath);
}
const defaultWorkloadPath = path.join(configLocation.configDirectory, DEFAULT_SEARCH_WORKLOAD_PATH);
if (!readJsonFile(defaultWorkloadPath, {
allowMissing: true
})) {
writeJsonFile(defaultWorkloadPath, DEFAULT_ATLAS_SEARCH_WORKLOAD_CONTENT);
createdFiles.push(defaultWorkloadPath);
}
let resolvedSearchConfig = nextSearchConfig;
let resolvedProjectConfig = null;
if (hasSearchProjectConfigs(nextSearchConfig)) {
const resolvedProject = resolveSearchProjectConfig(nextSearchConfig, projectId, {
defaultTerraformRootDir,
projectId,
terraformRootDir: defaultTerraformRootDir
});
resolvedSearchConfig = resolvedProject.config;
resolvedProjectConfig = resolvedProject.projectConfig;
}
return {
config: resolvedSearchConfig,
configPath,
createdFiles,
projectConfig: resolvedProjectConfig,
rootConfig: nextSearchConfig,
updatedFiles
};
};
export const writeSearchRuntimeServicesToProjectConfig = (configPath, projectId, serviceUrls) => {
const existing = readJsonFile(configPath, {
allowMissing: true
});
if (!existing || !projectId) {
return false;
}
const existingProject = existing.projects?.[projectId] ?? {};
const existingServices = existingProject.runtime?.services ?? {};
if (existingServices.apiServiceUrl === serviceUrls.apiServiceUrl && existingServices.syncServiceUrl === serviceUrls.syncServiceUrl) {
return false;
}
const next = {
...existing,
projects: {
...(existing.projects ?? {}),
[projectId]: {
...existingProject,
runtime: {
...(existingProject.runtime ?? {}),
services: {
apiServiceUrl: serviceUrls.apiServiceUrl,
syncServiceUrl: serviceUrls.syncServiceUrl
}
}
}
}
};
writeJsonFile(configPath, next);
return true;
};