@controlplane/cli
Version:
Control Plane Corporation CLI
1,012 lines (1,011 loc) • 65.8 kB
JavaScript
"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) {