UNPKG

@puls-atlas/cli

Version:

The Puls Atlas CLI tool for managing Atlas projects

235 lines 10.7 kB
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);