UNPKG

@controlplane/cli

Version:

Control Plane Corporation CLI

893 lines 46.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.K8sPodTemplateProcessor = void 0; const helper_1 = require("../../util/helper"); 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 job related properties this.processJob(); // Process Pull Secrets this.processImagePullSecrets(); } /*** 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) { container.command = k8sContainer.command.join(' '); } // Process args if (k8sContainer.args) { 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) { 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; } processContainerPorts(container, k8sContainer) { if (!k8sContainer.ports) { return; } // Define container.ports = []; for (const k8sPort of k8sContainer.ports) { let protocol = 'http'; if (k8sPort.name) { this.schemeToPort[k8sPort.name] = k8sPort.containerPort; } if (k8sPort.protocol) { protocol = k8sPort.protocol; } if (this.mapper.overrideProtocol) { protocol = this.mapper.overrideProtocol; } const port = { protocol: protocol.toLowerCase(), number: k8sPort.containerPort, }; container.ports.push(port); } } 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}/${item.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; } // 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); } } } // 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) { const volumeMountIDs = new Set(); if (!Array.isArray(container.volumeMounts)) { this.raiseCustomK8sError(`'${this.propertyPath}.spec.containers[${containerIndex}].volumeMounts' must be an array`); } // Iterate over each volume mount and validate it for (const [containerVolumeMountIndex, containerVolumeMount] of container.volumeMounts.entries()) { if (!containerVolumeMount.name) { this.ensurePropertyPresence(`spec.containers[${containerIndex}].volumeMounts[${containerVolumeMountIndex}].name`); } // Validate that the referenced volume exists if (!nameToVolume[containerVolumeMount.name]) { let throwError = true; // Check if the volume mount is actually referring to a persistent volume mount if (this.k8sObject.kind == 'StatefulSet') { const k8sWorkload = this.k8sObject; for (const pvc of (_a = k8sWorkload.spec.volumeClaimTemplates) !== null && _a !== void 0 ? _a : []) { if (pvc.metadata.name == containerVolumeMount.name) { throwError = false; break; } } } if (throwError) { this.raiseCustomK8sError(`'${this.propertyPath}.spec.containers[${containerIndex}].volumeMounts[${containerVolumeMountIndex}].name' is referencing a volume name that does not exist in '${this.propertyPath}.spec.volumes'`); } } if (!containerVolumeMount.mountPath) { this.ensurePropertyPresence(`spec.containers[${containerIndex}].volumeMounts[${containerVolumeMountIndex}].mountPath`); } // Define a volume mount id let id = `${containerVolumeMount.name}@${containerVolumeMount.mountPath}`; if (containerVolumeMount.subPath) { id = `${id}@${containerVolumeMount.subPath}`; } // Ensure that every volume mount is unique if (volumeMountIDs.has(id)) { this.raiseCustomK8sError(`Container has more than one volume mount for the same path, duplicate volume mount: '${this.propertyPath}.spec.containers[${containerIndex}].volumeMounts[${containerVolumeMountIndex}]'`); } // Reserve volume mount id volumeMountIDs.add(id); // Validate that the specified sub path exists within a volume's items if (containerVolumeMount.subPath) { // Get k8s volume object by name const volume = nameToVolume[containerVolumeMount.name]; // Validate that the specified sub path exists within a ConfigMap items if (volume.configMap && volume.configMap.items) { this.validateSubPathToExistInVolumeItems(containerVolumeMount.subPath, volume.configMap.items, volume.name, containerIndex, containerVolumeMountIndex); } // Validate that the specified sub path exists within a Secret items if (volume.secret && volume.secret.items) { this.validateSubPathToExistInVolumeItems(containerVolumeMount.subPath, volume.secret.items, volume.name, containerIndex, containerVolumeMountIndex); } // Validate that the specified sub path exists within a projected source items if (volume.projected) { for (const projectedSource of volume.projected.sources) { if (projectedSource.configMap && projectedSource.configMap.items) { this.validateSubPathToExistInVolumeItems(containerVolumeMount.subPath, projectedSource.configMap.items, volume.name, containerIndex, containerVolumeMountIndex); } if (projectedSource.secret && projectedSource.secret.items) { this.validateSubPathToExistInVolumeItems(containerVolumeMount.subPath, projectedSource.secret.items, volume.name, containerIndex, containerVolumeMountIndex); } } } } } } if (container.lifecycle) { if (container.lifecycle.postStart) { if (!container.lifecycle.postStart.exec) { this.ensurePropertyPresence(`spec.containers[${containerIndex}].lifecycle.postStart.exec`); } if (!container.lifecycle.postStart.exec.command) { this.ensurePropertyPresence(`spec.containers[${containerIndex}].lifecycle.postStart.exec.command`); } if (!Array.isArray(container.lifecycle.postStart.exec.command)) { this.raiseCustomK8sError(`'${this.propertyPath}.spec.containers[${containerIndex}].lifecycle.postStart.exec.command' must be an array of strings`); } } if (container.lifecycle.preStop) { if (!container.lifecycle.preStop.exec) { this.ensurePropertyPresence(`spec.containers[${containerIndex}].lifecycle.preStop.exec`); } if (!container.lifecycle.preStop.exec.command) { this.ensurePropertyPresence(`spec.containers[${containerIndex}].lifecycle.preStop.exec.command`); } if (!Array.isArray(container.lifecycle.preStop.exec.command)) { this.raiseCustomK8sError(`'${this.propertyPath}.spec.containers[${containerIndex}].lifecycle.preStop.exec.command' must be an array of strings`); } } } if (container.livenessProbe) { this.validateContainerHealthCheck(container.livenessProbe, `spec.containers[${containerIndex}].livenessProbe`, availableSchemes); } if (container.readinessProbe) { this.validateContainerHealthCheck(container.readinessProbe, `spec.containers[${containerIndex}].readinessProbe`, availableSchemes); } } // Validate job related properties if (this.workload.spec.job) { const maxActiveDeadlineSeconds = 86400; // Validate Restart Policy if (this.pod.spec.restartPolicy && this.pod.spec.restartPolicy.toLowerCase() === 'always') { this.raiseCustomK8sError(`'${this.propertyPath}.spec.restartPolicy' must either be 'Never' or 'OnFailure' for workloads of type cron`); } // Validate Active Deadline Seconds if (this.pod.spec.activeDeadlineSeconds && this.pod.spec.activeDeadlineSeconds > maxActiveDeadlineSeconds) { this.raiseCustomK8sError(`'${this.propertyPath}.spec.activeDeadlineSeconds' must be less than or equal to ${maxActiveDeadlineSeconds}`); } } // Validate Pull Secrets const alreadyValidated = new Set(); if (this.pod.spec.imagePullSecrets) { if (!Array.isArray(this.pod.spec.imagePullSecrets)) { this.raiseCustomK8sError(`'${this.propertyPath}.spec.imagePullSecrets' must be an array`); } for (const [index, imagePullSecret] of this.pod.spec.imagePullSecrets.entries()) { if (!imagePullSecret.name) { this.ensurePropertyPresence(`spec.imagePullSecrets[${index}].name`); } // Skip already validated if (alreadyValidated.has(imagePullSecret.name)) { continue; } alreadyValidated.add(imagePullSecret.name); } } if (this.pod.spec.serviceAccountName) { if (!this.mapper.serviceAccountNameToObject.has(this.pod.spec.serviceAccountName)) { this.raiseCustomK8sError(`The referenced service account '${this.pod.spec.serviceAccountName}' was not found`); } // Get the referenced Service Account const serviceAccount = this.mapper.serviceAccountNameToObject.get(this.pod.spec.serviceAccountName); if (serviceAccount.imagePullSecrets) { if (!Array.isArray(serviceAccount.imagePullSecrets)) { this.raiseCustomK8sError(`The 'imagePullSecrets' property of the referenced service account '${this.pod.spec.serviceAccountName}' must be an array`); } for (const [index, imagePullSecret] of serviceAccount.imagePullSecrets.entries()) { if (!imagePullSecret.name) { this.ensurePropertyPresence(`The 'imagePullSecrets[${index}].name' property of the referenced service account '${this.pod.spec.serviceAccountName}' is required`); } // Skip already validated if (alreadyValidated.has(imagePullSecret.name)) { continue; } alreadyValidated.add(imagePullSecret.name); } } } } validateContainerHealthCheck(probe, property, availableSchemes) { let counter = 0; let port = undefined; const maxInitialDelaySeconds = 600; const maxPeriodSeconds = 60; const maxTimeoutSeconds = 60; const maxSuccessThreshold = 20; const maxFailureThreshold = 20; if (probe.exec) { counter++; } if (probe.httpGet) { if (probe.httpGet.httpHeaders) { if (!Array.isArray(probe.httpGet.httpHeaders)) { this.raiseCustomK8sError(`'${this.propertyPath}.${property}.httpGet.httpHeaders' must be an array`); } for (const [index, httpHeader] of probe.httpGet.httpHeaders.entries()) { if (!httpHeader.name) { this.ensurePropertyPresence(`${property}.httpGet.httpHeaders[${index}].name`); } if (!httpHeader.value) { this.ensurePropertyPresence(`${property}.httpGet.httpHeaders[${index}].value`); } } } if (probe.httpGet.port) { port = probe.httpGet.port; } counter++; } if (probe.tcpSocket) { if (!probe.tcpSocket.port) { this.ensurePropertyPresence(`${property}.tcpSocket.port`); } port = probe.tcpSocket.port; counter++; } if (probe.grpc) { if (!probe.grpc.port) { this.ensurePropertyPresence(`${property}.grpc.port`); } port = probe.grpc.port; counter++; } if (port) { if (typeof port === 'string') { const numericPort = Number(port); if (!isNaN(numericPort) && port.trim() !== '') { this.validatePortNumber(property, numericPort); } else if (!availableSchemes.includes(port.toLowerCase())) { this.raiseCustomK8sError(`The specified port '${port}' name does not exist within the specified container ports. Supported port names within the contianer are: [${availableSchemes.join(', ')}]`); } } else { this.validatePortNumber(property, port); } } if (probe.initialDelaySeconds && probe.initialDelaySeconds > maxInitialDelaySeconds) { this.raiseCustomK8sError(`'${this.propertyPath}.${property}.initialDelaySeconds' must be less than or equal to ${maxInitialDelaySeconds}`); } if (probe.periodSeconds && probe.periodSeconds > maxPeriodSeconds) { this.raiseCustomK8sError(`'${this.propertyPath}.${property}.periodSeconds' must be less than or equal to ${maxPeriodSeconds}`); } if (probe.timeoutSeconds && probe.timeoutSeconds > maxTimeoutSeconds) { this.raiseCustomK8sError(`'${this.propertyPath}.${property}.timeoutSeconds' must be less than or equal to ${maxTimeoutSeconds}`); } if (probe.successThreshold && probe.successThreshold > maxSuccessThreshold) { this.raiseCustomK8sError(`'${this.propertyPath}.${property}.successThreshold' must be less than or equal to ${maxSuccessThreshold}`); } if (probe.failureThreshold && probe.failureThreshold > maxFailureThreshold) { this.raiseCustomK8sError(`'${this.propertyPath}.${property}.failureThreshold' must be less than or equal to ${maxFailureThreshold}`); } if (counter > 1) { this.raiseCustomK8sError(`There must be ONLY one of the following defined in a probe: exec, httpGet, tcpSocket or grpc at '${this.propertyPath}.${property}'`); } } validatePortNumber(property, port) { if (port < 1 || port > 65535) { this.raiseCustomK8sError(`'${this.propertyPath}.${property}.port' must be a valid port number between 1 and 65535 (inclusive)`); } } validateVolumeItems(propertName, items, volumeIndex) { if (!Array.isArray(items)) { this.raiseCustomK8sError(`'${this.propertyPath}.spec.volumes[${volumeIndex}].${propertName}.items' must be an array`); } for (const [index, item] of items.entries()) { if (!item.key) { this.ensurePropertyPresence(`spec.volumes[${volumeIndex}].${propertName}.items[${index}].key`); } if (!item.path) { this.ensurePropertyPresence(`spec.volumes[${volumeIndex}].${propertName}.items[${index}].path`); } } } validateSubPathToExistInVolumeItems(subPath, items, volumeName, containerIndex, containerVolumeMountIndex) { let isSubPathFound = false; // Iterate over each item to find the specified sub path for (const item of items) { if (subPath == item.path) { isSubPathFound = true; break; } } if (!isSubPathFound) { this.raiseCustomK8sError(`'${this.propertyPath}.spec.containers[${containerIndex}].volumeMounts[${containerVolumeMountIndex}].subPath' value points to a path '${subPath}' that does not exist in volume '${volumeName}'. Please ensure that the subPath '${subPath}' matches one of the defined 'path' values in the referenced volume`); } } // Validation Helpers // ensurePropertyPresence(property) { (0, helper_1.ensurePropertyPresence)(`${this.propertyPath}.${property}`, this.filePath, this.k8sObject); } raiseCustomK8sError(message) { (0, helper_1.raiseCustomK8sError)(message, this.filePath, this.k8sObject); } getFormattedWarning(message) { return (0, helper_1.getFormattedWarning)(message, this.filePath, this.k8sObject); } } exports.K8sPodTemplateProcessor = K8sPodTemplateProcessor; //# sourceMappingURL=pod-template.js.map