@puls-atlas/cli
Version:
The Puls Atlas CLI tool for managing Atlas projects
312 lines • 16.9 kB
JavaScript
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)}`);
}
};