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