@puls-atlas/cli
Version:
The Puls Atlas CLI tool for managing Atlas projects
235 lines • 10.7 kB
JavaScript
import inquirer from 'inquirer';
import { ensureSearchTerraformRoot } from './terraformWorkflow.js';
import { getSearchProvider, listSearchProviders } from './providers/index.js';
import { readJsonFile } from '../../utils/file.js';
import { normalizeOptionalString } from '../../utils/value.js';
import { ensureSearchConfigSection, loadSearchConfig, resolveSearchConfigLocation } from './config/searchConfig.js';
import { persistSearchProviderRuntimeConfig, resolveSearchProviderRuntimeSelection } from './providerDeploy.js';
import { enableGcloudServices, getFirebaserc, logger, resolveProjectSelection } from '../../utils/index.js';
const SEARCH_CORE_GCLOUD_SERVICES = ['eventarc.googleapis.com', 'eventarcpublishing.googleapis.com', 'firestore.googleapis.com', 'logging.googleapis.com', 'pubsub.googleapis.com', 'run.googleapis.com', 'secretmanager.googleapis.com', 'storage.googleapis.com'];
const SEARCH_INIT_PROVIDER_RUNTIME_OPTION_KEYS = ['clusterLocation', 'clusterName', 'createCluster', 'platform', 'zone'];
const resolveSearchProviderRuntimeServices = searchConfig => {
const providerRuntimeConfig = searchConfig?.deploy?.providerRuntime;
if (providerRuntimeConfig?.platform === 'compute') {
return providerRuntimeConfig.compute?.assignPublicIp === true ? ['compute.googleapis.com'] : ['compute.googleapis.com', 'iap.googleapis.com'];
}
if (providerRuntimeConfig?.platform === 'gke') {
return ['container.googleapis.com', 'compute.googleapis.com'];
}
return [];
};
export const resolveSearchInitGcloudServices = searchConfig => [...SEARCH_CORE_GCLOUD_SERVICES, ...resolveSearchProviderRuntimeServices(searchConfig)];
const createProviderChoiceLabel = providerName => {
const provider = getSearchProvider(providerName);
return provider.descriptor?.displayName ?? provider.name;
};
const resolveProviderFromFlag = providerName => {
const normalized = normalizeOptionalString(providerName);
if (!normalized) {
return null;
}
return getSearchProvider(normalized.toLowerCase()).name;
};
export const resolveSearchInitProviderSelection = async (options = {}, existingSearchConfig = null, dependencies = {}) => {
const explicitProvider = resolveProviderFromFlag(options.provider);
if (explicitProvider) {
return explicitProvider;
}
if (existingSearchConfig) {
return null;
}
if (options.interactive !== true) {
return null;
}
const prompt = dependencies.prompt ?? inquirer.prompt;
const providers = listSearchProviders();
const {
providerName
} = await prompt([{
choices: providers.map(name => ({
name: `${createProviderChoiceLabel(name)} (${name})`,
value: name
})),
default: 'typesense',
message: 'Choose the Atlas search provider for this project:',
name: 'providerName',
type: 'select'
}]);
return getSearchProvider(providerName).name;
};
const hasSearchInitProviderRuntimeConfig = searchConfig => normalizeOptionalString(searchConfig?.deploy?.providerRuntime?.platform) !== null;
const hasSearchInitProviderRuntimeOptions = (options = {}) => SEARCH_INIT_PROVIDER_RUNTIME_OPTION_KEYS.some(optionKey => {
if (optionKey === 'createCluster') {
return options.createCluster === true;
}
return normalizeOptionalString(options[optionKey]) !== null;
});
const shouldResolveSearchInitProviderRuntime = (options = {}, projectSelection = null, searchConfig = null) => {
if (!projectSelection) {
return false;
}
if (hasSearchInitProviderRuntimeConfig(searchConfig)) {
return false;
}
return options.interactive === true || hasSearchInitProviderRuntimeOptions(options);
};
const createSearchInitRuntimePrompt = (options, dependencies = {}) => {
if (options.interactive === true) {
return dependencies.prompt;
}
return async () => {
throw new Error('Atlas search init received incomplete provider runtime options. ' + 'Pass the remaining runtime flags or re-run with --interactive.');
};
};
const appendUniqueFilePath = (filePaths = [], filePath = undefined) => {
if (filePaths.includes(filePath)) {
return filePaths;
}
return filePaths.concat(filePath);
};
const reloadSearchInitConfigResult = (configResult, projectSelection, dependencies = {}, cwd = process.cwd()) => {
const loadSearchConfigImpl = dependencies.loadSearchConfig ?? loadSearchConfig;
const reloadedConfig = loadSearchConfigImpl(cwd, {
projectId: projectSelection?.projectId ?? null
});
return {
...configResult,
config: reloadedConfig.config,
projectConfig: reloadedConfig.projectConfig ?? configResult.projectConfig,
rootConfig: reloadedConfig.rootConfig ?? configResult.rootConfig,
updatedFiles: configResult.createdFiles.includes(configResult.configPath) || configResult.updatedFiles.includes(configResult.configPath) ? configResult.updatedFiles : appendUniqueFilePath(configResult.updatedFiles, configResult.configPath)
};
};
const logSearchInitOutcome = (loggerImpl, {
configResult,
terraformResult,
projectSelection,
gcloudServices
}) => {
loggerImpl.summary('Atlas search init summary', [{
label: 'Config',
value: configResult.configPath
}, {
label: 'Provider',
value: configResult.config.provider
}, {
label: 'Terraform root',
value: terraformResult.rootPath
}, projectSelection ? {
label: 'Project',
value: projectSelection.projectId
} : null, projectSelection?.environment ? {
label: 'Environment',
value: projectSelection.environment
} : null, projectSelection ? {
label: 'Enabled Google Cloud APIs',
value: gcloudServices.join(', ')
} : null]);
loggerImpl.summary('Created config files', configResult.createdFiles.concat(terraformResult.createdFiles), {
emptyMessage: 'No new config files were created.'
});
loggerImpl.summary('Updated files', configResult.updatedFiles.concat(terraformResult.updatedFiles), {
emptyMessage: 'No files were updated.'
});
if (!projectSelection) {
loggerImpl.warning('Skipped Google Cloud API activation because no project could be resolved. ' + 'Re-run "atlas search init" with --project or --environment once .firebaserc is available.');
}
};
const resolveSearchInitProjectSelection = async (options, dependencies = {}, cwd = process.cwd()) => {
const getFirebasercImpl = dependencies.getFirebaserc ?? getFirebaserc;
const resolveProjectSelectionImpl = dependencies.resolveProjectSelection ?? resolveProjectSelection;
const explicitProject = normalizeOptionalString(options.project);
const explicitEnvironment = normalizeOptionalString(options.environment);
const hasExplicitProjectSelection = explicitProject !== null || explicitEnvironment !== null;
const firebaserc = getFirebasercImpl({
allowMissing: true
}, {
cwd
});
if (!hasExplicitProjectSelection && !firebaserc?.projects) {
return null;
}
return resolveProjectSelectionImpl({
...options,
environment: explicitEnvironment ?? undefined,
project: explicitProject ?? undefined
});
};
export const runSearchInit = async (options = {}, dependencies = {}, cwd = process.cwd()) => {
let spinner;
let spinnerStopped = false;
const loggerImpl = dependencies.logger ?? logger;
const ensureSearchConfigSectionImpl = dependencies.ensureSearchConfigSection ?? ensureSearchConfigSection;
const ensureSearchTerraformRootImpl = dependencies.ensureSearchTerraformRoot ?? ensureSearchTerraformRoot;
const enableGcloudServicesImpl = dependencies.enableGcloudServices ?? enableGcloudServices;
const persistSearchProviderRuntimeConfigImpl = dependencies.persistSearchProviderRuntimeConfig ?? persistSearchProviderRuntimeConfig;
const resolveProjectSelectionForInit = dependencies.resolveSearchInitProjectSelection ?? resolveSearchInitProjectSelection;
const resolveSearchProviderRuntimeSelectionImpl = dependencies.resolveSearchProviderRuntimeSelection ?? resolveSearchProviderRuntimeSelection;
try {
const {
configPath
} = resolveSearchConfigLocation(cwd);
const existingSearchConfig = readJsonFile(configPath, {
allowMissing: true
});
const selectedProviderName = await resolveSearchInitProviderSelection(options, existingSearchConfig, {
prompt: dependencies.prompt
});
const projectSelection = await resolveProjectSelectionForInit(options, dependencies, cwd);
let configResult = ensureSearchConfigSectionImpl(cwd, {
...(projectSelection ? {
environment: projectSelection.environment ?? undefined,
projectId: projectSelection.projectId
} : {}),
providerName: selectedProviderName
});
if (shouldResolveSearchInitProviderRuntime(options, projectSelection, configResult.config)) {
const providerRuntimeContext = {
config: configResult.config,
configPath: configResult.configPath,
environment: projectSelection.environment ?? null,
projectId: projectSelection.projectId
};
const selectedProviderRuntimeConfig = await resolveSearchProviderRuntimeSelectionImpl(providerRuntimeContext, options, {
prompt: createSearchInitRuntimePrompt(options, dependencies)
});
persistSearchProviderRuntimeConfigImpl(providerRuntimeContext, selectedProviderRuntimeConfig, dependencies);
configResult = reloadSearchInitConfigResult(configResult, projectSelection, dependencies, cwd);
}
const terraformResult = await ensureSearchTerraformRootImpl(configResult.config, cwd);
const gcloudServices = resolveSearchInitGcloudServices(configResult.config);
spinner = loggerImpl.spinner('Initializing Atlas search config...');
if (projectSelection) {
spinner.stop();
spinnerStopped = true;
const apiSpinner = loggerImpl.spinner(`Enabling required Google Cloud APIs for ${projectSelection.projectId}...`);
try {
enableGcloudServicesImpl(projectSelection.projectId, gcloudServices, {
runCommand: dependencies.runGcloudCommand
});
apiSpinner.succeed('Required Google Cloud APIs are enabled.');
} catch (error) {
apiSpinner.fail('Failed to enable required Google Cloud APIs.');
throw error;
}
} else {
spinner.succeed('Atlas search config is ready.');
}
if (spinnerStopped) {
loggerImpl.info('Atlas search config is ready.');
}
logSearchInitOutcome(loggerImpl, {
configResult,
gcloudServices,
projectSelection,
terraformResult
});
} catch (error) {
if (spinner && !spinnerStopped) {
spinner.fail('Failed to initialize Atlas search config.');
}
loggerImpl.error(error.message);
}
};
export { resolveSearchInitProjectSelection };
export default async options => runSearchInit(options);