UNPKG

@controlplane/cli

Version:

Control Plane Corporation CLI

1,025 lines (1,024 loc) 78.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.K8sPodTemplateProcessor = void 0; const helper_1 = require("../../util/helper"); const constants_1 = require("../../config/constants"); class K8sPodTemplateProcessor { constructor(filePath, propertyPath, k8sObject, workload, pod, mapper) { this.filePath = filePath; this.propertyPath = propertyPath; this.k8sObject = k8sObject; this.workload = workload; this.pod = pod; this.mapper = mapper; this.volumes = {}; this.schemeToPort = {}; } /*** Public Methods ***/ process() { // Validate pod this.validate(); // Process volumes this.processVolumes(); // Process containers this.processContainers(); // Process security context this.processSecurityContext(); // Process job related properties this.processJob(); // Process Pull Secrets this.processImagePullSecrets(); // Process firewall this.processFirewallConfig(); } /*** Private Methods ***/ // Volumes // processVolumes() { if (!this.pod.spec.volumes) { return; } for (const volume of this.pod.spec.volumes) { this.volumes[volume.name] = volume; } } // Containers // processContainers() { // Read the inherit env annotation once; it applies to every container of the workload const inheritEnv = this.getInheritEnvAnnotation(); // Iterate over each container for (const k8sContainer of this.pod.spec.containers) { const container = { name: k8sContainer.name, image: k8sContainer.image, }; // Allow GVC-level environment variables to flow into the container when requested if (inheritEnv !== undefined) { container.inheritEnv = inheritEnv; } // Process resources this.processContainerResources(container, k8sContainer); // Process command if (k8sContainer.command && k8sContainer.command.length > 0) { container.command = k8sContainer.command[0]; // Any remaining items, add them to the container.args if (k8sContainer.command.length > 1) { container.args = k8sContainer.command.slice(1); } } // Process args if (k8sContainer.args) { if (container.args !== undefined) { container.args.push(...k8sContainer.args); } else { container.args = k8sContainer.args; } } // Process working directory if (k8sContainer.workingDir) { container.workingDir = k8sContainer.workingDir; } // Process ports this.processContainerPorts(container, k8sContainer); // Process Env this.processContainerEnv(container, k8sContainer); // Process Volume Mounts this.processContainerVolumeMounts(container, k8sContainer); // Process Lifecycle this.processContainerLifecycle(container, k8sContainer); // Process Liveness Probe if (k8sContainer.livenessProbe) { container.livenessProbe = this.processContainerProbe(k8sContainer.livenessProbe); } // Process Readiness Probe if (k8sContainer.readinessProbe) { container.readinessProbe = this.processContainerProbe(k8sContainer.readinessProbe); } this.workload.spec.containers.push(container); } } processContainerResources(container, k8sContainer) { var _a, _b, _c, _d; let cpu = '50m'; let memory = '128Mi'; // Extract the resources from the Kubernetes container if (k8sContainer.resources) { // If limits is provided and there is at least CPU or memory specified, then let's handle limits as the one that has the resource definitions if (k8sContainer.resources.limits && (k8sContainer.resources.limits['cpu'] || k8sContainer.resources.limits['memory'])) { if (k8sContainer.resources.limits['cpu']) { cpu = k8sContainer.resources.limits['cpu']; } if (k8sContainer.resources.limits['memory']) { memory = k8sContainer.resources.limits['memory']; } } // Otherwise, let's try to extract the resources from the requests property else if (k8sContainer.resources.requests) { if (k8sContainer.resources.requests['cpu']) { cpu = k8sContainer.resources.requests['cpu']; } if (k8sContainer.resources.requests['memory']) { memory = k8sContainer.resources.requests['memory']; } } } container.cpu = cpu; container.memory = memory; container.minCpu = (_b = (_a = k8sContainer.resources) === null || _a === void 0 ? void 0 : _a.requests) === null || _b === void 0 ? void 0 : _b.cpu; container.minMemory = (_d = (_c = k8sContainer.resources) === null || _c === void 0 ? void 0 : _c.requests) === null || _d === void 0 ? void 0 : _d.memory; } processContainerPorts(container, k8sContainer) { // Return early if the container has no ports defined if (!k8sContainer.ports) { return; } // Initialize the list of ports on the container spec container.ports = []; // Iterate over all declared ports of the container for (const k8sPort of k8sContainer.ports) { // If the port has a name, map it to its numeric value if (k8sPort.name) { this.schemeToPort[k8sPort.name] = k8sPort.containerPort; } // Create a port object with the inferred protocol and port number const port = { protocol: this.inferProtocolForContainerPort(k8sContainer, k8sPort), number: k8sPort.containerPort, }; // Add this processed port to the container spec container.ports.push(port); } } inferProtocolForContainerPort(k8sContainer, k8sPort) { // Respect a global override when configured if (this.mapper.overrideProtocol) { // Normalize and return the override as the final protocol return this.mapper.overrideProtocol.toLowerCase(); } // Prepare a list to accumulate protocol candidates in precedence order const candidates = []; // Retrieve all Services associated with this Pod/Workload const svcs = this.mapper.getServicesForWorkload(this.pod, this.k8sObject); // Compute the Service ports that explicitly target this container port (by name or number) const svcPorts = this.matchingServicePortsForContainer(k8sPort, svcs); // Iterate over matched service ports to consider explicit appProtocol and port names for (const servicePort of svcPorts) { // Attempt to resolve protocol from Service port appProtocol const pFromAppProtocol = this.protocolFromAppProtocol(servicePort.appProtocol); // Prioritize app protocol over any other candidate if (pFromAppProtocol) { return pFromAppProtocol; } // Attempt to resolve protocol from Service port name prefix const pFromServicePortName = this.protocolFromPortName(servicePort.name); // Record the candidate when found if (pFromServicePortName) { candidates.push(pFromServicePortName); } } // Attempt to resolve protocol from the container port name (prefix-based) const pFromContainerPortName = this.protocolFromPortName(k8sPort.name); // Record the candidate when found if (pFromContainerPortName) { candidates.push(pFromContainerPortName); } // Attempt to resolve protocol from liveness/readiness probes bound to this exact port const pFromProbes = this.protocolFromProbesForPort(k8sContainer, k8sPort); // Record the candidate when found if (pFromProbes) { candidates.push(pFromProbes); } // Attempt to resolve protocol from a well-known numeric port (weak heuristic) const pFromNumber = this.protocolFromPortNumber(k8sPort.containerPort); // Record the candidate when found if (pFromNumber) { candidates.push(pFromNumber); } // Resolve disagreements deterministically according to specificity ranking const resolved = this.pickMostSpecificProtocol(candidates); // Return the resolved protocol or default to tcp when nothing matched return resolved !== null && resolved !== void 0 ? resolved : 'tcp'; } matchingServicePortsForContainer(k8sPort, svcs) { var _a, _b; // Initialize an accumulator for matched service ports const output = []; // Normalize the container port name for name-based targetPort matches const containerPortName = ((_a = k8sPort.name) !== null && _a !== void 0 ? _a : '').trim().toLowerCase(); // Extract the numeric container port value for comparisons const containerPortNumber = k8sPort.containerPort; // Iterate over each candidate Service for (const svc of svcs) { // Skip if the service has no spec ports if (!svc.spec || !svc.spec.ports || !Array.isArray(svc.spec.ports)) { continue; } // Iterate over each declared Service port for (const servicePort of svc.spec.ports) { // Resolve the effective target port, prefer targetPort, fallback to port if necessary const targetPort = ((_b = servicePort.targetPort) !== null && _b !== void 0 ? _b : servicePort.port); // Match numeric targetPort directly if (typeof targetPort === 'number') { // Record a match when numbers are equal if (targetPort === containerPortNumber) { output.push(servicePort); } // Continue to next service port continue; } // Match string targetPort which may be a number-like string or a name if (typeof targetPort === 'string') { // Normalize the string representation const targetPortString = targetPort.trim().toLowerCase(); // Attempt to coerce numeric-looking strings const targetPortAsNumber = Number(targetPortString); // Record a match when the numeric coercion equals the container port if (!Number.isNaN(targetPortAsNumber) && targetPortAsNumber === containerPortNumber) { output.push(servicePort); continue; } // Record a match when the targetPort name equals the container port name if (targetPortString && targetPortString === containerPortName) { output.push(servicePort); } } } } // Return all matched service ports return output; } protocolFromAppProtocol(appProtocol) { // Normalize the input to a lowercase, trimmed string const normalized = this.normalizeProtocol(appProtocol); // Return early when the input is empty or missing if (!normalized) { return undefined; } // Split into parts on common delimiters: '/', ',', '|', and whitespace const parts = normalized.split(/[,\s|/]+/).filter(Boolean); // Initialize a list to hold candidate protocols discovered from tokens const candidates = []; // Iterate over each part extracted from appProtocol for (const part of parts) { // Attempt to resolve the part into a supported protocol const possibleProtocol = this.resolveProtocolToken(part); // Record the candidate when found if (possibleProtocol) { candidates.push(possibleProtocol); } } // Return early when no token produced a mapping if (candidates.length === 0) { return undefined; } // Select the most specific protocol among all token-derived candidates return this.pickMostSpecificProtocol(candidates); } resolveProtocolToken(token) { // Normalize the token to ensure consistent comparisons const possibleProtocol = token.trim().toLowerCase(); // Return undefined for empty tokens if (!possibleProtocol) { return undefined; } // Attempt a direct lookup in the known prefixes table const direct = constants_1.K8_NAME_PREFIX_PROTOCOLS[possibleProtocol]; // Return immediately when a direct match exists if (direct) { return direct; } // Extract the prefix before '-', '_' or '.' to follow common naming styles const prefix = possibleProtocol.split(/[-_.]/, 1)[0]; // Attempt a prefix-based lookup as a fallback return constants_1.K8_NAME_PREFIX_PROTOCOLS[prefix]; } pickMostSpecificProtocol(protocols) { // Initialize a de-duplication set while preserving discovery order const seen = new Set(protocols); // Prefer gRPC when present if (seen.has('grpc')) { return 'grpc'; } // Prefer HTTP/2 next if (seen.has('http2')) { return 'http2'; } // Prefer HTTP next if (seen.has('http')) { return 'http'; } // Prefer TCP last (still explicit over undefined) if (seen.has('tcp')) { return 'tcp'; } // Return undefined when no candidates exist return undefined; } protocolFromPortName(name) { // Normalize the provided name to a lowercase, trimmed string const normalized = this.normalizeProtocol(name); // Return early when no usable name is present if (!normalized) { return undefined; } // Extract the prefix before '-', '_' or '.' to follow common naming styles const prefix = normalized.split(/[-_.]/, 1)[0]; // Look up the protocol using the extracted prefix return constants_1.K8_NAME_PREFIX_PROTOCOLS[prefix]; } protocolFromProbesForPort(k8sContainer, k8sPort) { var _a; // Cache the numeric container port value const containerPortNumber = k8sPort.containerPort; // Normalize the container port name for name-based probe matching const containerPortName = ((_a = k8sPort.name) !== null && _a !== void 0 ? _a : '').trim().toLowerCase(); // Check readiness probe when present if (k8sContainer.readinessProbe) { // Prefer gRPC mapping when the probe targets this port if (k8sContainer.readinessProbe.grpc && this.probePortMatchesContainerPort(k8sContainer.readinessProbe.grpc.port, containerPortNumber, containerPortName)) { // Return grpc when matched return 'grpc'; } // Map HTTP/HTTPS to 'http' when the probe targets this port if (k8sContainer.readinessProbe.httpGet && this.probePortMatchesContainerPort(k8sContainer.readinessProbe.httpGet.port, containerPortNumber, containerPortName)) { // Return http when matched return 'http'; } // Fallback to 'tcp' when the TCP probe targets this port if (k8sContainer.readinessProbe.tcpSocket && this.probePortMatchesContainerPort(k8sContainer.readinessProbe.tcpSocket.port, containerPortNumber, containerPortName)) { // Return tcp when matched return 'tcp'; } } // Check liveness probe when present if (k8sContainer.livenessProbe) { // Prefer gRPC mapping when the probe targets this port if (k8sContainer.livenessProbe.grpc && this.probePortMatchesContainerPort(k8sContainer.livenessProbe.grpc.port, containerPortNumber, containerPortName)) { // Return grpc when matched return 'grpc'; } // Map HTTP/HTTPS to 'http' when the probe targets this port if (k8sContainer.livenessProbe.httpGet && this.probePortMatchesContainerPort(k8sContainer.livenessProbe.httpGet.port, containerPortNumber, containerPortName)) { // Return http when matched return 'http'; } // Fallback to 'tcp' when the TCP probe targets this port if (k8sContainer.livenessProbe.tcpSocket && this.probePortMatchesContainerPort(k8sContainer.livenessProbe.tcpSocket.port, containerPortNumber, containerPortName)) { // Return tcp when matched return 'tcp'; } } // Return undefined when no probe maps cleanly to this port return undefined; } probePortMatchesContainerPort(probePort, containerPortNumber, containerPortName) { // Short-circuit when probe has no port if (probePort === undefined) { return false; } // Compare when probe port is numeric if (typeof probePort === 'number') { return probePort === containerPortNumber; } // Normalize the string representation const probePortString = probePort.trim().toLowerCase(); // Attempt to coerce numeric-looking strings const probePortAsNumber = Number(probePortString); // Compare numerically when coercion succeeds if (!Number.isNaN(probePortAsNumber)) { return probePortAsNumber === containerPortNumber; } // Compare by name when a non-empty string name is present if (probePortString) { return probePortString === containerPortName; } // Return false when string is empty return false; } protocolFromPortNumber(portNumber) { // Return undefined when the input is not a finite number if (typeof portNumber !== 'number' || !Number.isFinite(portNumber)) { return undefined; } // Read the protocol mapping from the well-known ports table return constants_1.K8S_WELL_KNOWN_PORTS[portNumber]; } normalizeProtocol(value) { // Return undefined when the input is missing if (!value) { return undefined; } // Trim surrounding whitespace from the input const trimmed = value.trim(); // Return undefined when the trimmed value is empty if (!trimmed) { return undefined; } // Convert to lowercase for consistent downstream comparisons return trimmed.toLowerCase(); } processContainerEnv(container, k8sContainer) { var _a; // Expand envFrom sources into individual environment variables const expandedEnv = this.expandContainerEnvFrom(k8sContainer); // Skip when the container declares no environment variables at all if (!k8sContainer.env && expandedEnv.size == 0) { return; } // Accumulate explicitly declared environment variables const explicitEnv = []; for (const k8sEnv of (_a = k8sContainer.env) !== null && _a !== void 0 ? _a : []) { const env = { name: k8sEnv.name, }; if (k8sEnv.value) { env.value = k8sEnv.value; } if (k8sEnv.valueFrom) { if (k8sEnv.valueFrom.fieldRef) { env.value = `cpln://reference/${k8sEnv.valueFrom.fieldRef.fieldPath}`; } if (k8sEnv.valueFrom.secretKeyRef) { let secretLink = `cpln://secret/${k8sEnv.valueFrom.secretKeyRef.name}`; if (k8sEnv.valueFrom.secretKeyRef.key) { secretLink = `${secretLink}.${k8sEnv.valueFrom.secretKeyRef.key}`; } // Set env value env.value = secretLink; // Map secret name to workload name this.mapper.mapSecretToWorkload(k8sEnv.valueFrom.secretKeyRef.name, this.workload); } if (k8sEnv.valueFrom.configMapKeyRef) { const resolvedName = this.mapper.resolveConfigMapName(k8sEnv.valueFrom.configMapKeyRef.name); let secretLink = `cpln://secret/${resolvedName}`; if (k8sEnv.valueFrom.configMapKeyRef.key) { secretLink = `${secretLink}.${k8sEnv.valueFrom.configMapKeyRef.key}`; } // Set env value env.value = secretLink; // Map secret name to workload name this.mapper.mapSecretToWorkload(resolvedName, this.workload); } } explicitEnv.push(env); } // An explicitly declared variable takes precedence over an envFrom expansion with the same name for (const env of explicitEnv) { expandedEnv.delete(env.name); } // Define container.env = [...expandedEnv.values(), ...explicitEnv]; } /** * Expands all envFrom sources of a container into individual environment variables. * A key present in multiple sources takes the value of the last source, matching K8s behavior. * * @param {K8sPodContainer} k8sContainer - The K8s container whose envFrom sources are expanded. * @returns {Map<string, EnvVar>} The expanded environment variables keyed by variable name. */ expandContainerEnvFrom(k8sContainer) { var _a; const expandedEnv = new Map(); // Skip when the container declares no envFrom sources if (!k8sContainer.envFrom) { return expandedEnv; } // Iterate over each envFrom source in declaration order for (const source of k8sContainer.envFrom) { const prefix = (_a = source.prefix) !== null && _a !== void 0 ? _a : ''; // Expand every data key of the referenced ConfigMap if (source.configMapRef) { this.expandConfigMapEnvFrom(expandedEnv, source.configMapRef, prefix, k8sContainer.name); } // Expand every data key of the referenced Secret if (source.secretRef) { this.expandSecretEnvFrom(expandedEnv, source.secretRef, prefix, k8sContainer.name); } } return expandedEnv; } /** * Expands an envFrom ConfigMap reference into individual environment variables. * * @param {Map<string, EnvVar>} expandedEnv - The accumulator of expanded environment variables. * @param {K8sEnvFromSourceReference} configMapRef - The envFrom ConfigMap reference to expand. * @param {string} prefix - The prefix prepended to every expanded variable name. * @param {string} containerName - The name of the container being processed, used in warnings. */ expandConfigMapEnvFrom(expandedEnv, configMapRef, prefix, containerName) { var _a; const configMap = this.mapper.configMapNameToObject.get(configMapRef.name); // A missing non-optional ConfigMap is rejected during validation, so a miss here is an optional source if (!configMap) { this.mapper.warnings.push(`Skipped optional 'envFrom' ConfigMap '${configMapRef.name}' on container '${containerName}' because the ConfigMap is not part of the conversion input.`); return; } // ConfigMaps may have been renamed to avoid collisions with Secrets const resolvedName = this.mapper.resolveConfigMapName(configMapRef.name); // Only 'data' entries become environment variables; 'binaryData' is never exposed as env for (const key of Object.keys((_a = configMap.data) !== null && _a !== void 0 ? _a : {})) { this.addExpandedEnvVar(expandedEnv, `${prefix}${key}`, `cpln://secret/${resolvedName}.${key}`, resolvedName); } } /** * Expands an envFrom Secret reference into individual environment variables. * * @param {Map<string, EnvVar>} expandedEnv - The accumulator of expanded environment variables. * @param {K8sEnvFromSourceReference} secretRef - The envFrom Secret reference to expand. * @param {string} prefix - The prefix prepended to every expanded variable name. * @param {string} containerName - The name of the container being processed, used in warnings. */ expandSecretEnvFrom(expandedEnv, secretRef, prefix, containerName) { var _a, _b; const k8sSecret = this.mapper.secretNameToObject.get(secretRef.name); // The Secret keys are unknown when the Secret is not part of the conversion input, so expansion is impossible if (!k8sSecret) { this.mapper.warnings.push(`Unable to expand 'envFrom' Secret '${secretRef.name}' on container '${containerName}' because the Secret is not part of the conversion input. Include the Secret in the converted files, or add its keys to the container 'env' manually.`); return; } // Only dictionary secrets expose their keys individually; docker, opaque and userpass convert to a single value if (!(0, helper_1.convertsToDictionarySecret)(k8sSecret)) { this.mapper.warnings.push(`Unable to expand 'envFrom' Secret '${secretRef.name}' on container '${containerName}' because it does not convert to a dictionary secret whose keys are individually addressable. Add its values to the container 'env' manually.`); return; } // Union of both key sources; K8s merges 'stringData' into 'data' at creation time const keys = new Set([...Object.keys((_a = k8sSecret.data) !== null && _a !== void 0 ? _a : {}), ...Object.keys((_b = k8sSecret.stringData) !== null && _b !== void 0 ? _b : {})]); for (const key of keys) { this.addExpandedEnvVar(expandedEnv, `${prefix}${key}`, `cpln://secret/${secretRef.name}.${key}`, secretRef.name); } } /** * Adds a single expanded environment variable to the accumulator and maps its secret to the workload. * * @param {Map<string, EnvVar>} expandedEnv - The accumulator of expanded environment variables. * @param {string} name - The environment variable name, including any envFrom prefix. * @param {string} value - The cpln secret reference used as the variable value. * @param {string} secretName - The Control Plane secret name backing the variable. */ addExpandedEnvVar(expandedEnv, name, value, secretName) { // Environment variable names starting with 'CPLN_' are reserved for system use if (name.startsWith('CPLN_')) { this.mapper.warnings.push(`Skipped environment variable '${name}' expanded from 'envFrom' because names starting with 'CPLN_' are reserved for system use.`); return; } // A duplicate key keeps its original position but takes the value of the last source expandedEnv.set(name, { name, value }); // Map secret name to workload name this.mapper.mapSecretToWorkload(secretName, this.workload); } /** * Reads and validates the inherit env annotation of the workload. * * @returns {boolean | undefined} The annotation value as a boolean, or undefined when the annotation is absent. */ getInheritEnvAnnotation() { const value = (0, helper_1.getCplnAnnotation)(constants_1.K8S_CPLN_ANNOTATION_INHERIT_ENV, this.k8sObject, this.pod.metadata); if (value === undefined) { return undefined; } // Only explicit booleans are accepted if (value !== 'true' && value !== 'false') { this.raiseCustomK8sError(`Annotation '${constants_1.K8S_CPLN_ANNOTATION_INHERIT_ENV}' must be either 'true' or 'false', found '${value}'`); } return value === 'true'; } processContainerVolumeMounts(container, k8sContainer) { var _a, _b; if (!k8sContainer.volumeMounts || k8sContainer.volumeMounts.length == 0) { return; } // Define container.volumes = []; // Iterate over each k8s volume mount for (const k8sVolumeMount of k8sContainer.volumeMounts) { if (!this.volumes[k8sVolumeMount.name]) { // Check if the volume mount is referring to a persistent volume claim template if (this.k8sObject.kind == 'StatefulSet') { const k8sWorkload = this.k8sObject; // Iterate over each persistent volume claim in order to found what the volume mount is referring to for (const pvc of (_a = k8sWorkload.spec.volumeClaimTemplates) !== null && _a !== void 0 ? _a : []) { if (pvc.metadata.name == k8sVolumeMount.name) { // Construct volume set name const volumeSetName = `${this.k8sObject.metadata.name}-${pvc.metadata.name}`; // Construct the cpln container volume const uri = `cpln://volumeset/${volumeSetName}`; const path = `${k8sVolumeMount.mountPath}`; container.volumes.push({ uri, path, }); // Read the template's access modes directly; its name is unique only within this StatefulSet. // A dedicated (read-write-once) volume set restricts the workload to the stateful type if (!(0, helper_1.isSharedPvc)(pvc)) { this.mapper.workloadsWithDedicatedVolumeSets.add(this.workload); } // Exit for loop break; } } } // Handle next volume mount continue; } const k8sVolume = this.volumes[k8sVolumeMount.name]; // Mount Persistent Volume Claim as a volume if (k8sVolume.persistentVolumeClaim) { const claimName = k8sVolume.persistentVolumeClaim.claimName; // Assume the volume set name is the claim name of the PVC let volumeSetName = claimName; // A standalone PVC has a globally unique name, so its shared-ness can be resolved by name let isShared = this.mapper.sharedPvcNames.has(claimName); // If this is a StatefulSet, then let's explore its defined PVCs and update the volume set accordingly if (this.k8sObject.kind == 'StatefulSet') { const k8sWorkload = this.k8sObject; // Attempt to find the PVC within the workload, if found update the volume set name for (const pvc of (_b = k8sWorkload.spec.volumeClaimTemplates) !== null && _b !== void 0 ? _b : []) { if (pvc.metadata.name == claimName) { // Update the volume set name volumeSetName = `${this.k8sObject.metadata.name}-${claimName}`; // A template name is unique only within this StatefulSet, so read its access modes directly isShared = (0, helper_1.isSharedPvc)(pvc); // Stop the loop break; } } } // Construct the cpln container volume const uri = `cpln://volumeset/${volumeSetName}`; const path = `${k8sVolumeMount.mountPath}`; container.volumes.push({ uri, path, }); // A dedicated (read-write-once) volume set restricts the workload to the stateful type if (!isShared) { this.mapper.workloadsWithDedicatedVolumeSets.add(this.workload); } // Handle next volume mount continue; } // Mount ConfigMap as a volume if (k8sVolume.configMap) { const resolvedName = this.mapper.resolveConfigMapName(k8sVolume.configMap.name); this.mountVolume(container.volumes, k8sVolumeMount, resolvedName, k8sVolume.configMap.items); // Handle next volume mount continue; } // Mount Secret as a volume if (k8sVolume.secret) { this.mountVolume(container.volumes, k8sVolumeMount, k8sVolume.secret.secretName, k8sVolume.secret.items); // Handle next volume mount continue; } // Mount multiple projected volumes if (k8sVolume.projected) { for (const projectedSource of k8sVolume.projected.sources) { if (projectedSource.configMap) { const resolvedName = this.mapper.resolveConfigMapName(projectedSource.configMap.name); this.mountVolume(container.volumes, k8sVolumeMount, resolvedName, projectedSource.configMap.items); } if (projectedSource.secret) { this.mountVolume(container.volumes, k8sVolumeMount, projectedSource.secret.name, projectedSource.secret.items); } } // Handle next volume mount continue; } // Mount a shared volume if (k8sVolume.emptyDir) { const uri = `scratch://${k8sVolume.name}`; let path = k8sVolumeMount.mountPath; if (k8sVolumeMount.subPath) { path = `${path}/${k8sVolumeMount.subPath}`; } container.volumes.push({ uri, path, }); // Handle next volume mount } } } mountVolume(volumes, k8sVolumeMount, secretName, volumeItems) { // Map secret name to workload name this.mapper.mapSecretToWorkload(secretName, this.workload); // Prepare volume let uri = `cpln://secret/${secretName}`; let path = k8sVolumeMount.mountPath; // Process items and return if (volumeItems && volumeItems.length != 0) { // Iterate over each item and add a new volume for (const item of volumeItems) { // If a sub path is specified, then we will only pick and handle one item if (k8sVolumeMount.subPath) { // Find the path that the sub path is referencing if (k8sVolumeMount.subPath == item.path) { // Add new volume volumes.push({ uri: `${uri}.${item.key}`, path: path, }); // Exit iteration because we have found the item we are looking for break; } // Skip to next iteration in order to find the specified sub path continue; } // Add a new volume for each item volumes.push({ uri: `${uri}.${item.key}`, path: `${path}/${item.path}`, }); } return; } // Reference secret property if (k8sVolumeMount.subPath) { uri = `${uri}.${k8sVolumeMount.subPath}`; } // Add a new volume volumes.push({ uri, path, }); } processContainerLifecycle(container, k8sContainer) { if (!k8sContainer.lifecycle) { return; } container.lifecycle = {}; if (k8sContainer.lifecycle.postStart) { container.lifecycle.postStart = k8sContainer.lifecycle.postStart; } if (k8sContainer.lifecycle.preStop) { container.lifecycle.preStop = k8sContainer.lifecycle.preStop; } } processContainerProbe(k8sProbe) { const probe = { initialDelaySeconds: k8sProbe.initialDelaySeconds || 0, periodSeconds: k8sProbe.periodSeconds || 10, timeoutSeconds: k8sProbe.timeoutSeconds || 1, successThreshold: k8sProbe.successThreshold || 1, failureThreshold: k8sProbe.failureThreshold || 1, }; if (k8sProbe.exec) { probe.exec = k8sProbe.exec; } if (k8sProbe.httpGet) { let httpHeaders = []; let path = '/'; let port = 8080; let scheme = 'HTTP'; if (k8sProbe.httpGet.httpHeaders) { httpHeaders = k8sProbe.httpGet.httpHeaders; } if (k8sProbe.httpGet.path) { path = k8sProbe.httpGet.path; } if (k8sProbe.httpGet.port) { port = this.convertProbePort(k8sProbe.httpGet.port); } if (k8sProbe.httpGet.scheme) { scheme = k8sProbe.httpGet.scheme; } probe.httpGet = { httpHeaders, path, port, scheme, }; } if (k8sProbe.tcpSocket) { probe.tcpSocket = { port: this.convertProbePort(k8sProbe.tcpSocket.port), }; } if (k8sProbe.grpc) { probe.grpc = { port: this.convertProbePort(k8sProbe.grpc.port), }; } return probe; } convertProbePort(port) { if (typeof port === 'string') { const numericPort = Number(port); if (!isNaN(numericPort) && port.trim() !== '') { return numericPort; } return this.schemeToPort[port]; } return port; } // Security Context // processSecurityContext() { // Skip if security context is not specified if (!this.pod.spec.securityContext) { return; } // Initialize the security options object this.workload.spec.securityOptions = {}; // Use fsGroup if set if (this.pod.spec.securityContext.fsGroup !== undefined) { this.workload.spec.securityOptions.filesystemGroupId = this.pod.spec.securityContext.fsGroup; } } // Job // processJob() { if (!this.workload.spec.job) { return; } // Set Restart Policy if (this.pod.spec.restartPolicy) { this.workload.spec.job.restartPolicy = this.pod.spec.restartPolicy; } // Set Active Deadline Seconds if (this.pod.spec.activeDeadlineSeconds) { this.workload.spec.job.activeDeadlineSeconds = this.pod.spec.activeDeadlineSeconds; } } // Pull Secrets // processImagePullSecrets() { var _a; const alreadyAdded = new Set(); // Read pull secrets straight from the pod if (this.pod.spec.imagePullSecrets) { for (const imagePullSecret of this.pod.spec.imagePullSecrets) { // Skip already added if (alreadyAdded.has(imagePullSecret.name)) { continue; } this.mapper.imagePullSecrets.push(`//secret/${imagePullSecret.name}`); alreadyAdded.add(imagePullSecret.name); } } // Read pull secrets from the service account attached to the pod if (this.pod.spec.serviceAccountName) { const serviceAccount = this.mapper.serviceAccountNameToObject.get(this.pod.spec.serviceAccountName); for (const imagePullSecret of (_a = serviceAccount.imagePullSecrets) !== null && _a !== void 0 ? _a : []) { // Skip already added if (alreadyAdded.has(imagePullSecret.name)) { continue; } this.mapper.imagePullSecrets.push(`//secret/${imagePullSecret.name}`); alreadyAdded.add(imagePullSecret.name); } } } // Firewall Config // processFirewallConfig() { var _a, _b; // Initialize firewall config with defaults this.workload.spec.firewallConfig = { external: { inboundAllowCIDR: [], outboundAllowCIDR: ['0.0.0.0/0'], }, internal: { inboundAllowType: 'none', // Internal firewall 'none' by default inboundAllowWorkload: [], }, }; // Process the internal firewall before the public exposure check, which requires pod labels this.processInternalFirewall(); // If the pod has no metadata labels, skip public exposure check if (!((_a = this.pod.metadata) === null || _a === void 0 ? void 0 : _a.labels)) { return; } // By default, this workload is not exposed publicly unless proven otherwise let isPubliclyExposed = false; // Iterate over services and attempt to find the service with type LoadBalancer that is owned by this pod for (const service of this.mapper.services) { if (!service.spec || !service.spec.selector) { continue; } // A pod is publicly exposed to the internet if it connected to a LoadBalancer service and matches the labels of the pod if (service.spec.type === 'LoadBalancer' && this.mapper.isSelectorSubset(service.spec.selector, this.pod.metadata.labels)) { isPubliclyExposed = true; break; } } // If the service check doesn't indicate public exposure, then attempt to find that in ingresses if (!isPubliclyExposed && this.mapper.ingresses.length > 0) { // Iterate over each ingress and attempt to determine whether this workload should be exposed to the internet or not for (const ing of this.mapper.ingresses) { // Skip if required properties were not specified if (!ing.spec || !ing.spec.rules) { continue; } // Flag to determine whether to kill this loop or not let killLoop = false; // Iterate over spec rules for (const rule of ing.spec.rules) { // Skip if required properties were not specified if (!rule.http || !rule.http.paths) { continue; } // Iterate over http paths for (const path of rule.http.paths) { // Skip if required properties were not specified if (!path.backend || !path.backend.service || !path.backend.service.name) { continue; } // Find the target service that this ingress is referring to const targetSvc = this.mapper.services.find((s) => s.metadata.name === path.backend.service.name); // Check if the target service matches the pod labels if (targetSvc && ((_b = targetSvc.spec) === null || _b === void 0 ? void 0 : _b.selector) && this.mapper.isSelectorSubset(targetSvc.spec.selector, this.pod.metadata.labels)) { // An Ingress doesn't itself get a public IP (that's the Ingress Controller's job), // but it sits in front of one or more ClusterIP Services isPubliclyExposed = true; killLoop = true; break; } } // Kill nested loop if requested if (killLoop) { break; } } // Kill loop if requested if (killLoop) { break; } } } // If the workload should be exposed publicly, specify that in the workload firewall config if (isPubliclyExposed) { this.workload.spec.firewallConfig.external.inboundAllowCIDR = ['0.0.0.0/0']; } } /** * Determines the internal inbound allow type of the workload from the K8s Services that select it, * honoring the internal inbound allow type annotation as an explicit override. */ processInternalFirewall() { // A pod selected by a Service is reachable by every other pod in a K8s cluster, so 'same-gvc' preserves that reachability const services = this.mapper.getServicesForWorkload(this.pod, this.k8sObject); if (services.length > 0) { this.workload.spec.firewallConfig.internal.inboundAllowType = 'same-gvc'; } // Honor the annotation as an explicit override of the Service-based inference const annotationValue = (0, helper_1.getCplnAnnotation)(constants_1.K8S_CPLN_ANNOTATION_INTERNAL_INBOUND_ALLOW_TYPE, this.k8sObject, this.pod.metadata); if (annotationValue === undefined) { return; } // Only a fixed set of inbound allow types can be expressed through the annotation if (!constants_1.K8S_CPLN_ANNOTATION_INTERNAL_INBOUND_ALLOW_TYPES.includes(annotationValue)) { this.raiseCustomK8sError(`Annotation '${constants_1.K8S_CPLN_ANNOTATION_INTERNAL_INBOUND_ALLOW_TYPE}' must be one of [${constants_1.K8S_CPLN_ANNOTATION_INTERNAL_INBOUND_ALLOW_TYPES.join(', ')}], found '${annotationValue}'`); } this.workload.spec.firewallConfig.internal.inboundAllowType = annotationValue; } // Validators // validate() { var _a; if (!this.pod.spec) { this.ensurePropertyPresence('spec'); } // Create a name to volume map in order to validate a container volume mounts later const nameToVolume = {}; // Validate volumes if specified if (this.pod.spec.volumes) { if (!Array.isArray(this.pod.spec.volumes)) { this.raiseCustomK8sError(`'${this.propertyPath}.spec.volumes' must be an array`); } // Iterate over each volume and validate it for (const [volumeIndex, volume] of this.pod.spec.volumes.entries()) { if (!volume.name) { this.ensurePropertyPresence(`spec.volumes[${volumeIndex}].name`); } // Validate Persistent Volume Claim if (volume.persistentVolumeClaim) { if (!volume.persistentVolumeClaim.claimName) { this.ensurePropertyPresence(`spec.volumes[${volumeIndex}].persistentVolumeClaim.claimName`); } // Validate that a persistent volume claim with the given name exists if (!this.mapper.pvcToWorkload.has(volume.persistentVolumeClaim.claimName)) { this.raiseCustomK8sError(`The referenced persistent volume claim in 'spec.volumes[${volumeIndex}].persistentVolumeClaim.claimName' was not found`); } // Get the workload name that is mapped to the persistent volume claim const mountedWorkloadName = this.mapper.pvcToWorkload.get(volume.persistentVolumeClaim.claimName); // Validate that there is no other workload is mounting this volume if (mountedWorkloadName && this.workload.name != mountedWorkloadName) { this.raiseCustomK8sError(`The referenced persistent volume claim in 'spec.volumes[${volumeIndex}].persistentVolumeClaim.claimName' is already mounted to a different workload '${mountedWorkloadName}'. Each workload should have unique storage`); } } // Validate ConfigMap if specified if (volume.configMap) { if (!volume.configMap.name) { this.ensurePropertyPresence(`spec.volumes[${volumeIndex}].configMap.name`); } if (volume.configMap.items) { this.validateVolumeItems('configMap', volume.configMap.items, volumeIndex); } } // Validate Secret if specified if (volume.secret) { if (!volume.secret.secretName) { this.ensurePropertyPresence(`spec.volumes[${volumeIndex}].secret.secretName`);