UNPKG

@puls-atlas/cli

Version:

The Puls Atlas CLI tool for managing Atlas projects

312 lines 16.9 kB
import { normalizeOptionalString } from '../../../../utils/value.js'; import { getCommandErrorMessage, isGcloudResourceNotFoundError, runGcloudFileCommand } from '../../../../utils/gcloud.js'; import { AIRBYTE_SERVICE_SECRET_KEYS, resolveAirbyteServiceManagedResourceNames } from './terraform.js'; const AIRBYTE_BOOTLOADER_DIAGNOSTIC_EVENT_LIMIT = 5; const AIRBYTE_BOOTLOADER_DIAGNOSTIC_LOG_LINE_LIMIT = 20; const AIRBYTE_BOOTLOADER_DIAGNOSTIC_TAIL_LINES = 80; const listServiceClusterPods = async (terraformConnection, namespace, dependencies = {}) => { const { readServiceClusterJsonResource } = dependencies; if (typeof readServiceClusterJsonResource !== 'function') { return []; } const podList = await readServiceClusterJsonResource(terraformConnection, `/api/v1/namespaces/${encodeURIComponent(namespace)}/pods`, dependencies); return Array.isArray(podList?.items) ? podList.items : []; }; const listServiceClusterEvents = async (terraformConnection, namespace, dependencies = {}) => { const { readServiceClusterJsonResource } = dependencies; if (typeof readServiceClusterJsonResource !== 'function') { return []; } const eventList = await readServiceClusterJsonResource(terraformConnection, `/api/v1/namespaces/${encodeURIComponent(namespace)}/events`, dependencies); return Array.isArray(eventList?.items) ? eventList.items : []; }; const readServiceClusterPodLog = async (terraformConnection, namespace, podName, containerName, options = {}, dependencies = {}) => { const { readServiceClusterTextResource } = dependencies; if (typeof readServiceClusterTextResource !== 'function') { return null; } const searchParams = new URLSearchParams({ tailLines: String(options.tailLines ?? AIRBYTE_BOOTLOADER_DIAGNOSTIC_TAIL_LINES) }); if (containerName) { searchParams.set('container', containerName); } if (options.previous === true) { searchParams.set('previous', 'true'); } try { return await readServiceClusterTextResource(terraformConnection, `/api/v1/namespaces/${encodeURIComponent(namespace)}/pods/${encodeURIComponent(podName)}/log?${searchParams.toString()}`, dependencies); } catch { return null; } }; const getServiceClusterResourceTimestamp = resource => normalizeOptionalString(resource?.lastTimestamp ?? resource?.eventTime ?? resource?.metadata?.creationTimestamp ?? resource?.metadata?.managedFields?.at(-1)?.time) ?? ''; const sortServiceClusterResourcesByTimestamp = resources => [...resources].sort((left, right) => getServiceClusterResourceTimestamp(left).localeCompare(getServiceClusterResourceTimestamp(right))); const getServiceClusterPodContainerStatuses = pod => [...(Array.isArray(pod?.status?.initContainerStatuses) ? pod.status.initContainerStatuses : []).map(containerStatus => ({ ...containerStatus, kind: 'init-container' })), ...(Array.isArray(pod?.status?.containerStatuses) ? pod.status.containerStatuses : []).map(containerStatus => ({ ...containerStatus, kind: 'container' }))]; const resolveServiceClusterContainerFailure = containerStatus => { if (containerStatus?.state?.terminated) { return { exitCode: containerStatus.state.terminated.exitCode, message: normalizeOptionalString(containerStatus.state.terminated.message), reason: normalizeOptionalString(containerStatus.state.terminated.reason), state: 'terminated' }; } if (containerStatus?.state?.waiting) { return { message: normalizeOptionalString(containerStatus.state.waiting.message), reason: normalizeOptionalString(containerStatus.state.waiting.reason), state: 'waiting' }; } if (containerStatus?.lastState?.terminated) { return { exitCode: containerStatus.lastState.terminated.exitCode, message: normalizeOptionalString(containerStatus.lastState.terminated.message), reason: normalizeOptionalString(containerStatus.lastState.terminated.reason), state: 'terminated' }; } return null; }; const createAirbyteBootloaderEventEntries = (events, bootloaderPrefix) => { const matchingEvents = events.filter(event => { const involvedObjectName = normalizeOptionalString(event?.involvedObject?.name); const message = normalizeOptionalString(event?.message); return involvedObjectName?.includes(bootloaderPrefix) === true || message?.includes(bootloaderPrefix) === true; }); const selectedEvents = (matchingEvents.length > 0 ? matchingEvents : sortServiceClusterResourcesByTimestamp(events)).slice(-AIRBYTE_BOOTLOADER_DIAGNOSTIC_EVENT_LIMIT); return selectedEvents.map(event => { const timestamp = getServiceClusterResourceTimestamp(event); const reason = normalizeOptionalString(event?.reason) ?? 'Unknown'; const message = normalizeOptionalString(event?.message) ?? 'No event message reported.'; return `${timestamp ? `[${timestamp}] ` : ''}${reason}: ${message}`; }); }; const createAirbyteBootloaderLogEntries = logText => String(logText).split(/\r?\n/u).map(line => line.trimEnd()).filter(Boolean).slice(-AIRBYTE_BOOTLOADER_DIAGNOSTIC_LOG_LINE_LIMIT); const findAirbyteIngressForHost = (ingressList, host) => { const expectedHost = normalizeOptionalString(host); if (!expectedHost || !Array.isArray(ingressList?.items)) { return null; } return ingressList.items.find(ingress => Array.isArray(ingress?.spec?.rules) ? ingress.spec.rules.some(rule => normalizeOptionalString(rule?.host) === expectedHost) : false) ?? null; }; const inspectAirbyteBootloaderFailure = async (serviceConfig, terraformConnection, dependencies = {}) => { const { namespace, releaseName } = resolveAirbyteServiceManagedResourceNames(serviceConfig); const bootloaderPrefix = `${releaseName}-airbyte-bootloader`; try { const [pods, events] = await Promise.all([listServiceClusterPods(terraformConnection, namespace, dependencies), listServiceClusterEvents(terraformConnection, namespace, dependencies)]); const bootloaderPod = sortServiceClusterResourcesByTimestamp(pods.filter(pod => normalizeOptionalString(pod?.metadata?.name)?.includes(bootloaderPrefix) === true)).at(-1); const containerStatuses = getServiceClusterPodContainerStatuses(bootloaderPod); const failingContainerStatus = containerStatuses.find(containerStatus => Boolean(resolveServiceClusterContainerFailure(containerStatus))) ?? containerStatuses[0]; const containerFailure = resolveServiceClusterContainerFailure(failingContainerStatus); const podScheduledCondition = (bootloaderPod?.status?.conditions ?? []).find(condition => condition.type === 'PodScheduled' && condition.status === 'False'); const containerName = normalizeOptionalString(failingContainerStatus?.name) ?? normalizeOptionalString(bootloaderPod?.spec?.containers?.[0]?.name); const logText = bootloaderPod ? (await readServiceClusterPodLog(terraformConnection, namespace, bootloaderPod.metadata.name, containerName, { previous: false, tailLines: AIRBYTE_BOOTLOADER_DIAGNOSTIC_TAIL_LINES }, dependencies)) ?? (await readServiceClusterPodLog(terraformConnection, namespace, bootloaderPod.metadata.name, containerName, { previous: true, tailLines: AIRBYTE_BOOTLOADER_DIAGNOSTIC_TAIL_LINES }, dependencies)) : null; const eventEntries = createAirbyteBootloaderEventEntries(events, bootloaderPrefix); if (!bootloaderPod && eventEntries.length === 0 && !logText) { return { namespace, status: 'not-found', warning: `Atlas could not find a retained Airbyte bootloader pod or related Kubernetes events in namespace ${namespace}. ` + 'The Helm release may have been cleaned up before inspection.' }; } return { containerFailure, containerKind: normalizeOptionalString(failingContainerStatus?.kind), containerName, eventEntries, logEntries: logText ? createAirbyteBootloaderLogEntries(logText) : [], namespace, nodeName: normalizeOptionalString(bootloaderPod?.spec?.nodeName), podMessage: normalizeOptionalString(bootloaderPod?.status?.message), podName: normalizeOptionalString(bootloaderPod?.metadata?.name), podPhase: normalizeOptionalString(bootloaderPod?.status?.phase), podReason: normalizeOptionalString(bootloaderPod?.status?.reason), scheduledMessage: normalizeOptionalString(podScheduledCondition?.message), scheduledReason: normalizeOptionalString(podScheduledCondition?.reason), status: 'available' }; } catch (error) { return { namespace, status: 'unavailable', warning: `Atlas could not inspect Airbyte bootloader failure details. ${error.message}` }; } }; export const validateAirbyteDeployment = async (_context, serviceConfig, terraformConnection, dependencies = {}) => { const { readServiceClusterJsonResource } = dependencies; if (typeof readServiceClusterJsonResource !== 'function') { return undefined; } const { namespace } = resolveAirbyteServiceManagedResourceNames(serviceConfig); const ingressList = await readServiceClusterJsonResource(terraformConnection, `/apis/networking.k8s.io/v1/namespaces/${encodeURIComponent(namespace)}/ingresses`, dependencies); const matchingIngress = findAirbyteIngressForHost(ingressList, serviceConfig.host); if (matchingIngress) { return { host: serviceConfig.host, ingressName: normalizeOptionalString(matchingIngress?.metadata?.name) ?? null, namespace }; } const existingIngressNames = Array.isArray(ingressList?.items) ? ingressList.items.map(ingress => normalizeOptionalString(ingress?.metadata?.name)).filter(Boolean) : []; throw new Error(`Atlas deployed Airbyte workloads, but Kubernetes has no Ingress for host ${serviceConfig.host} in namespace ${namespace}. ` + `This usually means the configured Airbyte Helm ingress values do not match the installed chart schema.${existingIngressNames.length > 0 ? ` Existing ingresses: ${existingIngressNames.join(', ')}.` : ''}`); }; export const getAirbyteTerraformImportTargets = (context, serviceConfig, terraformConnection, dependencies = {}) => { const managedResourceNames = resolveAirbyteServiceManagedResourceNames(serviceConfig); const targets = [{ address: 'module.atlas_service.google_compute_global_address.this', describeArgs: ['compute', 'addresses', 'describe', managedResourceNames.globalAddressName, `--project=${context.projectId}`, '--global', '--format=value(name)'], id: `projects/${context.projectId}/global/addresses/${managedResourceNames.globalAddressName}`, kind: 'global-address', name: managedResourceNames.globalAddressName }, { address: 'module.atlas_service.google_compute_managed_ssl_certificate.this', describeArgs: ['compute', 'ssl-certificates', 'describe', managedResourceNames.managedCertificateName, `--project=${context.projectId}`, '--global', '--format=value(name)'], id: `projects/${context.projectId}/global/sslCertificates/${managedResourceNames.managedCertificateName}`, kind: 'managed-ssl-certificate', name: managedResourceNames.managedCertificateName }, { address: 'module.atlas_service.kubernetes_service_account_v1.this', existsImpl: () => dependencies.doesServiceClusterServiceAccountExist?.(terraformConnection, managedResourceNames.namespace, managedResourceNames.serviceAccountName, dependencies) ?? false, id: `${managedResourceNames.namespace}/${managedResourceNames.serviceAccountName}`, kind: 'service-account', name: managedResourceNames.serviceAccountName }, { address: 'module.atlas_service.kubernetes_role_v1.service_account', existsImpl: () => dependencies.doesServiceClusterRoleExist?.(terraformConnection, managedResourceNames.namespace, managedResourceNames.roleName, dependencies) ?? false, id: `${managedResourceNames.namespace}/${managedResourceNames.roleName}`, kind: 'role', name: managedResourceNames.roleName }, { address: 'module.atlas_service.kubernetes_role_binding_v1.service_account', existsImpl: () => dependencies.doesServiceClusterRoleBindingExist?.(terraformConnection, managedResourceNames.namespace, managedResourceNames.roleBindingName, dependencies) ?? false, id: `${managedResourceNames.namespace}/${managedResourceNames.roleBindingName}`, kind: 'role-binding', name: managedResourceNames.roleBindingName }, { address: 'module.atlas_service.kubernetes_secret_v1.config', existsImpl: async () => Boolean(await dependencies.readServiceClusterSecret?.(terraformConnection, managedResourceNames.namespace, managedResourceNames.configSecretName, dependencies)), id: `${managedResourceNames.namespace}/${managedResourceNames.configSecretName}`, kind: 'secret', name: managedResourceNames.configSecretName }, { address: 'module.atlas_service.helm_release.this', existsImpl: () => dependencies.doesServiceClusterHelmReleaseExist?.(terraformConnection, managedResourceNames.namespace, managedResourceNames.releaseName, dependencies) ?? false, id: `${managedResourceNames.namespace}/${managedResourceNames.releaseName}`, kind: 'helm-release', name: managedResourceNames.releaseName }]; if (serviceConfig.createNamespace !== false) { targets.splice(2, 0, { address: 'module.atlas_service.kubernetes_namespace_v1.this[0]', existsImpl: () => dependencies.doesServiceClusterNamespaceExist?.(terraformConnection, managedResourceNames.namespace, dependencies) ?? false, id: managedResourceNames.namespace, kind: 'namespace', name: managedResourceNames.namespace }); } return targets; }; export const resolveAirbyteRuntimeInputs = async (_context, serviceConfig, terraformConnection, dependencies = {}) => { const managedResourceNames = resolveAirbyteServiceManagedResourceNames(serviceConfig); const existingSecret = await dependencies.readServiceClusterSecret?.(terraformConnection, managedResourceNames.namespace, managedResourceNames.configSecretName, dependencies); if (!existingSecret) { return {}; } return { adminPassword: normalizeOptionalString(existingSecret[AIRBYTE_SERVICE_SECRET_KEYS.adminPassword]), databasePassword: normalizeOptionalString(existingSecret[AIRBYTE_SERVICE_SECRET_KEYS.databasePassword]) }; }; export const logAirbyteTerraformApplyFailureDetails = async (serviceConfig, loggerImpl, dependencies = {}) => { const inspection = await inspectAirbyteBootloaderFailure(serviceConfig, dependencies.terraformConnection, dependencies); if (inspection.status === 'unavailable' || inspection.status === 'not-found') { loggerImpl.warning(inspection.warning); return; } const primaryReason = inspection.containerFailure?.reason ?? inspection.scheduledReason ?? inspection.podReason; const primaryMessage = inspection.containerFailure?.message ?? inspection.scheduledMessage ?? inspection.podMessage ?? inspection.eventEntries[0] ?? null; const exitCode = inspection.containerFailure?.exitCode; loggerImpl.summary('Airbyte bootloader failure', [{ label: 'Namespace', value: inspection.namespace }, { label: 'Pod', tone: inspection.podName ? 'warning' : 'muted', value: inspection.podName ?? 'not retained' }, inspection.podPhase ? { label: 'Phase', tone: 'warning', value: inspection.podPhase } : null, inspection.containerName ? { label: 'Container', value: `${inspection.containerName}${inspection.containerKind ? ` (${inspection.containerKind})` : ''}` } : null, primaryReason ? { label: 'Reason', tone: 'warning', value: primaryReason } : null, typeof exitCode === 'number' ? { label: 'Exit code', value: String(exitCode) } : null, inspection.nodeName ? { label: 'Node', value: inspection.nodeName } : null, primaryMessage ? { label: 'Message', value: primaryMessage } : null]); if (inspection.eventEntries.length > 0) { loggerImpl.section('Recent Airbyte bootloader events', inspection.eventEntries); } if (inspection.logEntries.length > 0) { loggerImpl.section('Airbyte bootloader log tail', inspection.logEntries); } }; export const doesAirbyteTerraformImportTargetExist = async (target, dependencies = {}) => { if (typeof target.existsImpl === 'function') { return target.existsImpl(); } if (!Array.isArray(target.describeArgs)) { return false; } const runGcloudFileCommandImpl = dependencies.runGcloudFileCommand ?? ((args, options) => runGcloudFileCommand(args, options, dependencies.runCommand)); try { runGcloudFileCommandImpl(target.describeArgs, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }); return true; } catch (error) { if (isGcloudResourceNotFoundError(error)) { return false; } throw new Error(`Could not inspect existing Atlas service ${target.kind} ${target.name}. ${getCommandErrorMessage(error)}`); } };