UNPKG

@puls-atlas/cli

Version:

The Puls Atlas CLI tool for managing Atlas projects

298 lines 13.4 kB
import path from 'path'; import { resolveSearchBackfillJobDeployment } from './backfillJob.js'; import { resolveSearchManagedArtifactBuckets } from './artifactBucket.js'; import { getEnabledSearchEventarcCollectionPlans, SEARCH_EVENTARC_DEFAULTS } from './eventarc.js'; import { resolveSearchCloudRunServiceAccountEmail } from './config/searchConfig.js'; import { resolveSearchDlqBucketConfig, resolveSearchTaskQueueConfig } from './planning.js'; import { resolveSearchRuntimeSecretAccessContract } from './runtimeSecrets.js'; import { getAtlasGeneratedTerraformDir } from '../../utils/atlas.js'; import { writeJsonFile } from '../../utils/file.js'; import { normalizeOptionalString } from '../../utils/value.js'; import { ATLAS_SEARCH_TASK_QUEUE_DEFAULTS } from '../../utils/taskQueue.js'; import { SEARCH_SOURCE_RUNNER_REQUESTED_SOURCE_ENV, getEnabledScheduledSearchSourcePlans } from './sources/scheduled.js'; import { SEARCH_SOURCE_RUNNER_CONTAINER_NAME, resolveSearchSourceRunnerJobDeployment } from './sources/runnerJob.js'; import { getConfiguredSearchFirestoreEventarcDatabase, getConfiguredSearchFirestoreEventarcTriggerRegion } from './firestoreEventarc.js'; export const SEARCH_TERRAFORM_CONTRACT_VERSION = 6; export const SEARCH_TERRAFORM_INPUT_VARIABLE = 'atlas_search_tfvars_path'; const ATLAS_SEARCH_DLQ_BUCKET_RUNTIME_ROLE = 'roles/storage.objectAdmin'; const SEARCH_SOURCE_SCHEDULER_OAUTH_SCOPE = 'https://www.googleapis.com/auth/cloud-platform'; const DEFAULT_BACKFILL_JOB_EXECUTION = { maxRetries: 0, parallelism: 1, taskCount: 1, timeout: '3600s' }; const normalizeLabelValue = value => { const normalized = normalizeOptionalString(value); if (!normalized) { return null; } const normalizedValue = normalized.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-+|-+$/g, '').slice(0, 63).replace(/-+$/g, ''); return normalizedValue.length > 0 ? normalizedValue : null; }; const sortObjectKeys = objectValue => Object.fromEntries(Object.entries(objectValue).sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey))); const createSearchTerraformLabels = (context, runtime) => sortObjectKeys(Object.fromEntries(Object.entries({ 'atlas-component': 'search', 'atlas-environment': normalizeLabelValue(context.environment), 'atlas-managed-by': 'terraform', 'atlas-project': normalizeLabelValue(context.projectId), 'atlas-provider': normalizeLabelValue(context.config.provider), 'atlas-runtime': runtime }).filter(([, value]) => value !== null))); const createSearchTerraformBucketPayload = ({ bucketName, location, retentionDays = null }) => ({ bucket_name: bucketName, location, ...(retentionDays !== null ? { retention_days: retentionDays } : {}), uniform_bucket_level_access: true }); const getConfiguredServiceAccountEmail = context => resolveSearchCloudRunServiceAccountEmail(context.config, context.projectId); const getConfiguredVpcAccess = context => { const vpcAccess = context.config?.deploy?.cloudRun?.vpcAccess; if (!vpcAccess || typeof vpcAccess !== 'object') { return null; } const network = normalizeOptionalString(vpcAccess.network); if (!network) { return null; } return { egress: normalizeOptionalString(vpcAccess.egress) ?? 'private-ranges-only', network, subnetwork: normalizeOptionalString(vpcAccess.subnetwork) }; }; const createSearchTerraformBackfillJobPayload = context => { const deployment = resolveSearchBackfillJobDeployment(context); const runtimeEnvVars = sortObjectKeys(deployment.runtimeEnvironmentVariables); const searchConfigPayloadSize = 0; const warnings = []; const serviceAccountEmail = getConfiguredServiceAccountEmail(context); const vpcAccess = getConfiguredVpcAccess(context); return { payload: { contract_version: 1, environment: context.environment ?? null, image: deployment.image, job_name: deployment.jobName, max_retries: DEFAULT_BACKFILL_JOB_EXECUTION.maxRetries, parallelism: DEFAULT_BACKFILL_JOB_EXECUTION.parallelism, project_id: deployment.projectId, region: deployment.region, resource_labels: createSearchTerraformLabels(context, 'backfill-job'), runtime_env_vars: runtimeEnvVars, service_account_email: serviceAccountEmail, task_count: DEFAULT_BACKFILL_JOB_EXECUTION.taskCount, timeout: DEFAULT_BACKFILL_JOB_EXECUTION.timeout, ...(vpcAccess ? { vpc_access: vpcAccess } : {}) }, searchConfigPayloadSize, warnings }; }; const createSearchTerraformSourceRunnerJobPayload = context => { const deployment = resolveSearchSourceRunnerJobDeployment(context); if (!deployment) { return null; } const runtimeEnvVars = sortObjectKeys(deployment.runtimeEnvironmentVariables); const serviceAccountEmail = getConfiguredServiceAccountEmail(context); const vpcAccess = getConfiguredVpcAccess(context); return { contract_version: 1, environment: context.environment ?? null, image: deployment.image, job_name: deployment.jobName, max_retries: DEFAULT_BACKFILL_JOB_EXECUTION.maxRetries, parallelism: DEFAULT_BACKFILL_JOB_EXECUTION.parallelism, project_id: deployment.projectId, region: deployment.region, resource_labels: createSearchTerraformLabels(context, 'source-runner-job'), runtime_env_vars: runtimeEnvVars, ...(Object.keys(deployment.runtimeSecretEnvironmentVariables ?? {}).length > 0 ? { secret_env_vars: sortObjectKeys(deployment.runtimeSecretEnvironmentVariables) } : {}), service_account_email: serviceAccountEmail, task_count: DEFAULT_BACKFILL_JOB_EXECUTION.taskCount, timeout: DEFAULT_BACKFILL_JOB_EXECUTION.timeout, ...(vpcAccess ? { vpc_access: vpcAccess } : {}) }; }; const createSearchTerraformSourceSchedulerPayload = context => { const deployment = resolveSearchSourceRunnerJobDeployment(context); if (!deployment) { return null; } const scheduledSourcePlans = getEnabledScheduledSearchSourcePlans(context.config); if (scheduledSourcePlans.length === 0) { return null; } return { container_name: SEARCH_SOURCE_RUNNER_CONTAINER_NAME, contract_version: 1, jobs: scheduledSourcePlans.map(sourcePlan => ({ name: sourcePlan.schedulerJobName, schedule: sourcePlan.schedule, source_name: sourcePlan.sourceName, time_zone: sourcePlan.timeZone })), oauth_scope: SEARCH_SOURCE_SCHEDULER_OAUTH_SCOPE, region: deployment.region, service_account_email: deployment.serviceAccountEmail, source_name_env_var: SEARCH_SOURCE_RUNNER_REQUESTED_SOURCE_ENV, source_runner_job_name: deployment.jobName, source_runner_region: deployment.region }; }; const createSearchTerraformFirestoreEventarcPayload = (context, options = {}) => { const deploymentRegion = resolveSearchBackfillJobDeployment(context).region; const configuredTriggerRegion = getConfiguredSearchFirestoreEventarcTriggerRegion(context, options); return { collections: getEnabledSearchEventarcCollectionPlans(context.config).map(collectionPlan => ({ collection_name: collectionPlan.collectionName, document_path_pattern: collectionPlan.documentPathPattern, trigger_name: collectionPlan.triggerName })), database: getConfiguredSearchFirestoreEventarcDatabase(context, options), event_data_content_type: 'application/protobuf', event_type: SEARCH_EVENTARC_DEFAULTS.eventType, region: deploymentRegion, resource_labels: createSearchTerraformLabels(context, 'firestore-sync'), service_name: 'atlas-search-sync', service_path_prefix: SEARCH_EVENTARC_DEFAULTS.pathPrefix, trigger_service_account: { account_id: SEARCH_EVENTARC_DEFAULTS.triggerServiceAccountId, display_name: SEARCH_EVENTARC_DEFAULTS.triggerServiceAccountDisplayName }, ...(configuredTriggerRegion && configuredTriggerRegion !== deploymentRegion ? { trigger_region: configuredTriggerRegion } : {}) }; }; const createSearchTerraformSyncTaskQueuePayload = context => { const taskQueueConfig = resolveSearchTaskQueueConfig(context); return { contract_version: 1, location: taskQueueConfig.location, name: taskQueueConfig.name, rate_limits: { max_concurrent_dispatches: ATLAS_SEARCH_TASK_QUEUE_DEFAULTS.maxConcurrentDispatches, max_dispatches_per_second: ATLAS_SEARCH_TASK_QUEUE_DEFAULTS.maxDispatchesPerSecond }, retry_config: { max_attempts: ATLAS_SEARCH_TASK_QUEUE_DEFAULTS.maxAttempts, max_backoff: ATLAS_SEARCH_TASK_QUEUE_DEFAULTS.maxBackoff, max_doublings: ATLAS_SEARCH_TASK_QUEUE_DEFAULTS.maxDoublings, max_retry_duration: ATLAS_SEARCH_TASK_QUEUE_DEFAULTS.maxRetryDuration, min_backoff: ATLAS_SEARCH_TASK_QUEUE_DEFAULTS.minBackoff }, stackdriver_logging_config: { sampling_ratio: ATLAS_SEARCH_TASK_QUEUE_DEFAULTS.logSamplingRatio } }; }; const createSearchTerraformArtifactBucketsPayload = context => resolveSearchManagedArtifactBuckets(context).map(bucketState => createSearchTerraformBucketPayload({ bucketName: bucketState.bucketName, location: bucketState.location })); const createSearchTerraformDlqBucketPayload = context => { const dlqBucketConfig = resolveSearchDlqBucketConfig(context); return createSearchTerraformBucketPayload({ bucketName: dlqBucketConfig.name, location: dlqBucketConfig.location, retentionDays: dlqBucketConfig.retentionDays }); }; const createSearchTerraformDlqBucketAccessPayload = context => { const serviceAccountEmail = getConfiguredServiceAccountEmail(context); if (!serviceAccountEmail) { return null; } return { bucket_name: resolveSearchDlqBucketConfig(context).name, role: ATLAS_SEARCH_DLQ_BUCKET_RUNTIME_ROLE, service_account_email: serviceAccountEmail }; }; const createSearchTerraformRuntimeSecretAccessPayload = context => { const runtimeSecretAccess = resolveSearchRuntimeSecretAccessContract(context); if (!runtimeSecretAccess.serviceAccountEmail || runtimeSecretAccess.secretAccess.length === 0) { return null; } return { contract_version: 1, role: runtimeSecretAccess.secretAccess[0].role, secret_names: runtimeSecretAccess.secretAccess.map(secretAccess => secretAccess.secretName), service_account_email: runtimeSecretAccess.serviceAccountEmail }; }; export const createSearchTerraformAdapter = (context, options = {}) => { const backfillJobAdapter = createSearchTerraformBackfillJobPayload(context); const sourceRunnerJobPayload = createSearchTerraformSourceRunnerJobPayload(context); const sourceSchedulerPayload = createSearchTerraformSourceSchedulerPayload(context); return { payload: { artifact_buckets: createSearchTerraformArtifactBucketsPayload(context), backfill_job: backfillJobAdapter.payload, contract_version: SEARCH_TERRAFORM_CONTRACT_VERSION, dlq_bucket: createSearchTerraformDlqBucketPayload(context), dlq_bucket_access: createSearchTerraformDlqBucketAccessPayload(context), environment: context.environment ?? null, firestore_eventarc: createSearchTerraformFirestoreEventarcPayload(context, options), project_id: context.projectId, region: backfillJobAdapter.payload.region, runtime_secret_access: createSearchTerraformRuntimeSecretAccessPayload(context), ...(sourceRunnerJobPayload ? { source_runner_job: sourceRunnerJobPayload } : {}), ...(sourceSchedulerPayload ? { source_scheduler: sourceSchedulerPayload } : {}), sync_task_queue: createSearchTerraformSyncTaskQueuePayload(context) }, searchConfigPayloadSize: backfillJobAdapter.searchConfigPayloadSize, warnings: backfillJobAdapter.warnings }; }; export const getSearchTerraformArtifactPath = (projectId, cwd = process.cwd()) => path.join(getAtlasGeneratedTerraformDir(cwd), 'search', `search.${projectId}.tfvars.json`); export const writeSearchTerraformArtifact = (context, options = {}, cwd = process.cwd()) => { const { writeJsonFileImpl = writeJsonFile } = options; const adapter = createSearchTerraformAdapter(context, options); const filePath = getSearchTerraformArtifactPath(context.projectId, cwd); writeJsonFileImpl(filePath, adapter.payload); return { contractVersion: adapter.payload.contract_version, filePath, payload: adapter.payload, searchConfigPayloadSize: adapter.searchConfigPayloadSize, warnings: adapter.warnings }; }; export const SEARCH_TERRAFORM_BACKFILL_JOB_CONTRACT_VERSION = SEARCH_TERRAFORM_CONTRACT_VERSION; export const SEARCH_TERRAFORM_BACKFILL_JOB_INPUT_VARIABLE = SEARCH_TERRAFORM_INPUT_VARIABLE; export const createSearchTerraformBackfillJobAdapter = createSearchTerraformAdapter; export const getSearchTerraformBackfillJobArtifactPath = getSearchTerraformArtifactPath; export const writeSearchTerraformBackfillJobArtifact = writeSearchTerraformArtifact; export default { SEARCH_TERRAFORM_BACKFILL_JOB_CONTRACT_VERSION, SEARCH_TERRAFORM_BACKFILL_JOB_INPUT_VARIABLE, SEARCH_TERRAFORM_CONTRACT_VERSION, SEARCH_TERRAFORM_INPUT_VARIABLE, createSearchTerraformAdapter, createSearchTerraformBackfillJobAdapter, getSearchTerraformArtifactPath, getSearchTerraformBackfillJobArtifactPath, writeSearchTerraformArtifact, writeSearchTerraformBackfillJobArtifact };