UNPKG

@controlplane/cli

Version:

Control Plane Corporation CLI

1,012 lines (1,011 loc) 65.8 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() { // Iterate over each container for (const k8sContainer of this.pod.spec.containers) { const container = { name: k8sContainer.name, image: k8sContainer.image, }; // 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) { if (!k8sContainer.env) { return; } // Define container.env = []; for (const k8sEnv of k8sContainer.env) { 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) { let secretLink = `cpln://secret/${k8sEnv.valueFrom.configMapKeyRef.name}`; 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(k8sEnv.valueFrom.configMapKeyRef.name, this.workload); } } container.env.push(env); } } 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, }); // 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) { // Assume the volume set name is the claim name of the PVC let volumeSetName = k8sVolume.persistentVolumeClaim.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 == k8sVolume.persistentVolumeClaim.claimName) { // Update the volume set name volumeSetName = `${this.k8sObject.metadata.name}-${k8sVolume.persistentVolumeClaim.claimName}`; // Stop the loop break; } } } // Construct the cpln container volume const uri = `cpln://volumeset/${volumeSetName}`; const path = `${k8sVolumeMount.mountPath}`; container.volumes.push({ uri, path, }); // Handle next volume mount continue; } // Mount ConfigMap as a volume if (k8sVolume.configMap) { this.mountVolume(container.volumes, k8sVolumeMount, k8sVolume.configMap.name, 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) { this.mountVolume(container.volumes, k8sVolumeMount, projectedSource.configMap.name, 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', inboundAllowWorkload: [], }, }; // 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.matchesSelector(this.pod.metadata.labels, service.spec.selector)) { 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 && this.matchesSelector(this.pod.metadata.labels, (_b = targetSvc.spec) === null || _b === void 0 ? void 0 : _b.selector)) { // 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']; } } matchesSelector(labels, selector) { if (!selector) { return false; } return Object.entries(selector).every(([key, value]) => labels[key] === value); } // 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`); } if (volume.secret.items) { this.validateVolumeItems('secret', volume.secret.items, volumeIndex); } } // Validate projected if specified if (volume.projected) { if (!volume.projected.sources) { this.ensurePropertyPresence(`spec.volumes[${volumeIndex}].projected.sources`); } if (!Array.isArray(volume.projected.sources)) { this.raiseCustomK8sError(`'${this.propertyPath}.spec.volumes[${volumeIndex}].projected.sources' must be an array`); } for (const [volumeProjectedSourceIndex, volumeProjectedSource] of volume.projected.sources.entries()) { // Validate ConfigMap if (volumeProjectedSource.configMap) { if (!volumeProjectedSource.configMap.name) { this.ensurePropertyPresence(`spec.volumes[${volumeIndex}].projected.sources[${volumeProjectedSourceIndex}].configMap.name`); } // Validate ConfigMap items if (volumeProjectedSource.configMap.items) { if (!Array.isArray(volumeProjectedSource.configMap.items)) { this.raiseCustomK8sError(`'${this.propertyPath}.spec.volumes[${volumeIndex}].projected.sources[${volumeProjectedSourceIndex}].configMap.items' must be an array`); } if (volumeProjectedSource.configMap.items.length == 0) { this.raiseCustomK8sError(`'${this.propertyPath}.spec.volumes[${volumeIndex}].projected.sources[${volumeProjectedSourceIndex}].configMap.items' must have at least one item`); } this.validateVolumeItems(`projected.sources[${volumeProjectedSourceIndex}].configMap`, volumeProjectedSource.configMap.items, volumeIndex); } else { this.ensurePropertyPresence(`spec.volumes[${volumeIndex}].projected.sources[${volumeProjectedSourceIndex}].configMap.items`); } } // Validate Secret if (volumeProjectedSource.secret) { if (!volumeProjectedSource.secret.name) { this.ensurePropertyPresence(`spec.volumes[${volumeIndex}].projected.sources[${volumeProjectedSourceIndex}].secret.name`); } // Validate Secret items if (volumeProjectedSource.secret.items) { if (!Array.isArray(volumeProjectedSource.secret.items)) { this.raiseCustomK8sError(`'${this.propertyPath}.spec.volumes[${volumeIndex}].projected.sources[${volumeProjectedSourceIndex}].secret.items' must be an array`); } if (volumeProjectedSource.secret.items.length == 0) { this.raiseCustomK8sError(`'${this.propertyPath}.spec.volumes[${volumeIndex}].projected.sources[${volumeProjectedSourceIndex}].secret.items' must have at least one item`); } this.validateVolumeItems(`projected.sources[${volumeProjectedSourceIndex}].secret`, volumeProjectedSource.secret.items, volumeIndex); } else { this.ensurePropertyPresence(`spec.volumes[${volumeIndex}].projected.sources[${volumeProjectedSourceIndex}].secret.items`); } } } } // Ensure volume names are unique if (nameToVolume[volume.name]) { this.raiseCustomK8sError(`'${this.propertyPath}.spec.volumes' has a duplicate name: '${volume.name}'`); } // Reserve volume name nameToVolume[volume.name] = volume; } } // Validate containers if (!this.pod.spec.containers) { this.ensurePropertyPresence('spec.containers'); } if (!Array.isArray(this.pod.spec.containers)) { this.raiseCustomK8sError(`'${this.propertyPath}.spec.containers' must be an array`); } if (this.pod.spec.containers.length == 0) { this.raiseCustomK8sError(`There must be at least one container defined in '${this.propertyPath}.spec.containers'`); } // Validate each container for (const [containerIndex, container] of this.pod.spec.containers.entries()) { const availableSchemes = []; if (!container.name) { this.ensurePropertyPresence(`spec.containers[${containerIndex}].name`); } if (container.name.startsWith('cpln-')) { this.raiseCustomK8sError(`'${this.propertyPath}.spec.containers[${containerIndex}].name' failed custom validation because container names cannot start with 'cpln-'`); } if (!container.image) { this.ensurePropertyPresence(`spec.containers[${containerIndex}].image`); } if (container.command && !Array.isArray(container.command)) { this.raiseCustomK8sError(`'${this.propertyPath}.spec.containers[${containerIndex}].command' must be an array of strings`); } if (container.args && !Array.isArray(container.args)) { this.raiseCustomK8sError(`'${this.propertyPath}.spec.containers[${containerIndex}].args' must be an array of strings`); } if (container.ports) { if (!Array.isArray(container.ports)) { this.raiseCustomK8sError(`'${this.propertyPath}.spec.containers[${containerIndex}].ports' must be an array`); } // Validate each port for (const [portIndex, port] of container.ports.entries()) { if (!port.containerPort) { this.ensurePropertyPresence(`spec.containers[${containerIndex}].ports[${portIndex}].containerPort`); } if (port.protocol && port.protocol.toLowerCase() === 'udp') { this.raiseCustomK8sError(`Protocol 'udp' is unsupported at spec.containers[${containerIndex}].ports[${portIndex}].protocol`); } if (port.name) { if (availableSchemes.includes(port.name)) { this.raiseCustomK8sError(`Port name must be unique, found duplicate name '${port.name}' at spec.containers[${containerIndex}].ports[${portIndex}].name`); } availableSchemes.push(port.name.toLowerCase()); } } } if (container.env) { if (!Array.isArray(container.env)) { this.raiseCustomK8sError(`'${this.propertyPath}.spec.containers[${containerIndex}].env' must be an array`); } // Validate each env for (const [envIndex, env] of container.env.entries()) { if (!env.name) { this.ensurePropertyPresence(`spec.containers[${containerIndex}].env[${envIndex}].name`); } if (env.name.startsWith('CPLN_')) { this.raiseCustomK8sError(`'${this.propertyPath}.spec.containers[${containerIndex}].env[${envIndex}].name' failed custom validation because environment variable names starting with 'CPLN_' are reserved for system use`); } if (env.value && env.valueFrom) { this.raiseCustomK8sError(`should either provide 'value' or 'valueFrom' but not both at '${this.propertyPath}.spec.containers[${containerIndex}].env[${envIndex}]'`); } if (env.valueFrom) { if (env.valueFrom.fieldRef && env.valueFrom.secretKeyRef && env.valueFrom.configMapKeyRef) { this.raiseCustomK8sError(`ONLY one of 'fieldRef' or 'secretKeyRef' or 'configMapKeyRef' can be provided at 'spec.containers[${containerIndex}].env[${envIndex}].valueFrom'`); } if (!env.valueFrom.fieldRef && !env.valueFrom.secretKeyRef && !env.valueFrom.configMapKeyRef) { this.raiseCustomK8sError(`You must either prvoide one of 'fieldRef' or 'secretKeyRef' or 'configMapKeyRef' at 'spec.containers[${containerIndex}].env[${envIndex}].valueFrom'`); } if (env.valueFrom.fieldRef && !env.valueFrom.fieldRef.fieldPath) { this.ensurePropertyPresence(`spec.containers[${containerIndex}].env[${envIndex}].valueFrom.fieldRef.fieldPath`); } if (env.valueFrom.secretKeyRef && !env.valueFrom.secretKeyRef.name) { this.ensurePropertyPresence(`spec.containers[${containerIndex}].env[${envIndex}].valueFrom.secretKeyRef.name`); } if (env.valueFrom.configMapKeyRef && !env.valueFrom.configMapKeyRef.name) { this.ensurePropertyPresence(`spec.containers[${containerIndex}].env[${envIndex}].valueFrom.configMapKeyRef.name`); } if (env.valueFrom.configMapKeyRef && env.valueFrom.configMapKeyRef.name && !this.mapper.configMapNameToObject.has(env.valueFrom.configMapKeyRef.name)) { this.raiseCustomK8sError(`'spec.containers[${containerIndex}].env[${envIndex}].valueFrom.configMapKeyRef.name' is referencing a ConfigMap that does not exist. Make sure the ConfigMap '${env.valueFrom.configMapKeyRef.name}' is specified within the convert path`); } } } } if (container.volumeMounts) {