@controlplane/cli
Version:
Control Plane Corporation CLI
407 lines • 21.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.K8sMapper = void 0;
const types_1 = require("../types");
const hpa_1 = require("./handlers/hpa");
class K8sMapper {
constructor(overrideProtocol) {
this.overrideProtocol = overrideProtocol;
this.k8sWorkloadNameToHpa = {};
this.workloadToSecretNames = new Map();
this.pvcToWorkload = new Map();
this.pvNameToObject = new Map();
this.storageClassNameToObject = new Map();
this.serviceAccountNameToObject = new Map();
this.configMapNameToObject = new Map();
this.endpointSlicesByService = new Map();
this.services = [];
this.ingresses = [];
this.imagePullSecrets = [];
this.warnings = [];
this.kindToK8sObject = {};
this.kindToEquivalent = new Map();
// Fill kind to equivalent
this.kindToEquivalent.set('ConfigMap', 'Secret');
this.kindToEquivalent.set('ReplicationController', 'Deployment');
this.kindToEquivalent.set('ReplicaSet', 'Deployment');
this.kindToEquivalent.set('StatefulSet', 'Deployment');
this.kindToEquivalent.set('Job', 'Deployment');
this.kindToEquivalent.set('CronJob', 'Deployment');
this.kindToEquivalent.set('DaemonSet', 'Deployment');
}
/*** Public Methods ***/
initialize(objects) {
// Map objects to their respective data structures
for (const object of objects) {
// Ensure that no object of the same kind has the same name
this.ensureUniqueNames(object);
// Assume this object is being mapped until proven otherwise
let isMapped = true;
// Start mapping objects to their respective data structures
switch (object.resource.kind) {
case 'StatefulSet':
this.handleK8sStatefulWorkload(object.resource);
break;
case 'StatefulSetList':
const k8sWorkloadList = object.resource;
for (const k8sWorkload of k8sWorkloadList.items) {
this.handleK8sStatefulWorkload(k8sWorkload);
}
break;
case 'ConfigMap':
const configMap = object.resource;
this.configMapNameToObject.set(configMap.metadata.name, configMap);
break;
case 'ConfigMapList':
const configMapList = object.resource;
for (const configMap of configMapList.items) {
this.configMapNameToObject.set(configMap.metadata.name, configMap);
}
break;
case 'PersistentVolumeClaim':
this.pvcToWorkload.set(object.resource.metadata.name, '');
break;
case 'PersistentVolumeClaimList':
const pvcList = object.resource;
for (const pvc of pvcList.items) {
this.pvcToWorkload.set(pvc.metadata.name, '');
}
break;
case 'PersistentVolume':
const pv = object.resource;
this.pvNameToObject.set(pv.metadata.name, pv);
break;
case 'PersistentVolumeList':
const pvList = object.resource;
for (const pv of pvList.items) {
this.pvNameToObject.set(pv.metadata.name, pv);
}
break;
case 'StorageClass':
const storageClass = object.resource;
this.storageClassNameToObject.set(storageClass.metadata.name, storageClass);
break;
case 'StorageClassList':
const storageClassList = object.resource;
for (const storageClass of storageClassList.items) {
this.storageClassNameToObject.set(storageClass.metadata.name, storageClass);
}
break;
case 'HorizontalPodAutoscaler':
this.handleHpa(object.filePath, object.resource);
break;
case 'HorizontalPodAutoscalerList':
const hpaList = object.resource;
for (const hpa of hpaList.items) {
this.handleHpa(object.filePath, hpa);
}
break;
case 'ServiceAccount':
const serviceAccount = object.resource;
this.serviceAccountNameToObject.set(serviceAccount.metadata.name, serviceAccount);
break;
case 'ServiceAccountList':
const serviceAccountList = object.resource;
for (const serviceAccount of serviceAccountList.items) {
this.serviceAccountNameToObject.set(serviceAccount.metadata.name, serviceAccount);
}
break;
case 'Service':
const service = object.resource;
this.services.push(service);
break;
case 'ServiceList':
const serviceList = object.resource;
this.services.push(...serviceList.items);
break;
case 'EndpointSlice':
const endpointSlice = object.resource;
this.handleEndpointSlice(endpointSlice);
break;
case 'EndpointSliceList':
const endpointSliceList = object.resource;
for (const endpointSlice of endpointSliceList.items) {
this.handleEndpointSlice(endpointSlice);
}
break;
case 'Ingress':
const ingress = object.resource;
this.ingresses.push(ingress);
break;
case 'IngressList':
const ingressList = object.resource;
this.ingresses.push(...ingressList.items);
break;
default:
isMapped = false;
break;
}
// Don't worry about this, this is put in place so we can avoid developer error;
// It helps sync between the kinds being mapped and the mappable kinds list
if (isMapped && !types_1.K8sMappableKinds.includes(object.resource.kind)) {
// As I said, don't worry about it; This exception will never fire on production because we will catch it in test
throw new Error(`DEVELOPER ERROR: K8s Kind: '${object.resource.kind}' needs to be added to the K8sMappableKinds array.`);
}
}
}
mapSecretToWorkload(secretName, workload) {
if (!this.workloadToSecretNames.has(workload)) {
this.workloadToSecretNames.set(workload, new Set([secretName]));
return;
}
// Get secret names that this workload is referring to
const secretNames = this.workloadToSecretNames.get(workload);
// Don't add the same secret twice to the map
if (secretNames.has(secretName)) {
return;
}
// Map secret to workload
secretNames.add(secretName);
}
getServicesForWorkload(pod, owner) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s;
// Derive the namespace for lookups, defaulting to "default" when Pod namespace is missing
const ns = (_d = (_b = (_a = pod.metadata) === null || _a === void 0 ? void 0 : _a.namespace) !== null && _b !== void 0 ? _b : (_c = owner.metadata) === null || _c === void 0 ? void 0 : _c.namespace) !== null && _d !== void 0 ? _d : 'default';
// Capture the Pod's labels as a simple string map for selector matching
const labels = ((_f = (_e = pod.metadata) === null || _e === void 0 ? void 0 : _e.labels) !== null && _f !== void 0 ? _f : {});
// Preserve the Pod name for EndpointSlice matching that may reference name-based hints
const podName = (_g = pod.metadata) === null || _g === void 0 ? void 0 : _g.name;
// Preserve the Pod IP for EndpointSlice address-level membership checks
const podIP = (_h = pod.status) === null || _h === void 0 ? void 0 : _h.podIP;
// Initialize the output accumulator that will collect matching Services
const output = [];
// Track seen Services by unique key to prevent duplicates across strategies
const seen = new Set();
// Begin strategy (1): Match Services with label selectors against the Pod's labels
for (const svc of this.services) {
// Skip Services that belong to a different namespace than the Pod
if (((_k = (_j = svc.metadata) === null || _j === void 0 ? void 0 : _j.namespace) !== null && _k !== void 0 ? _k : 'default') !== ns) {
continue;
}
// Extract the selector as a simple map for subset checks
const selector = ((_m = (_l = svc.spec) === null || _l === void 0 ? void 0 : _l.selector) !== null && _m !== void 0 ? _m : {});
// Ignore Services without selectors; those are handled via EndpointSlice membership
if (Object.keys(selector).length === 0) {
continue;
}
// If the Service selector is a subset of the Pod labels, consider it a match
if (this.isSelectorSubset(selector, labels)) {
// Add the Service once, guarding against duplicates across strategies
this.addServiceIfNotSeen(output, seen, svc);
}
}
// Begin strategy (2): Use EndpointSlice membership to include selectorless and custom-topology Services
for (const svc of this.services) {
// Respect namespace boundaries; only consider Services in the Pod's namespace
if (((_p = (_o = svc.metadata) === null || _o === void 0 ? void 0 : _o.namespace) !== null && _p !== void 0 ? _p : 'default') !== ns) {
continue;
}
// Resolve the Service name; if missing, the Service cannot be correlated to EndpointSlices
const svcName = (_q = svc.metadata) === null || _q === void 0 ? void 0 : _q.name;
if (!svcName) {
continue;
}
// Fetch all EndpointSlices associated with the Service via a precomputed index
const endpointSlices = this.endpointSlicesByService.get(this.serviceKey(ns, svcName));
// If no EndpointSlices are found, there is no membership evidence to consider
if (!endpointSlices || endpointSlices.length === 0) {
continue;
}
// Inspect each EndpointSlice for an address or targetRef that matches this Pod
for (const endpointSlice of endpointSlices) {
// Invoke a helper that encapsulates the slice -> pod matching logic
if (this.endpointSliceMatchesPod(endpointSlice, ns, podName, podIP)) {
// If at least one EndpointSlice confirms membership, include the Service once
this.addServiceIfNotSeen(output, seen, svc);
break;
}
}
}
// Begin strategy (3): If the owner is a StatefulSet, include its governing Service (spec.serviceName)
if ((owner === null || owner === void 0 ? void 0 : owner.kind) === 'StatefulSet') {
// Read the governing Service name from the owning StatefulSet
const svcName = (_r = owner.spec) === null || _r === void 0 ? void 0 : _r.serviceName;
// Only attempt lookup if a governing service name is provided
if (svcName) {
// Locate the Service by name within the same namespace as the Pod
const svc = this.services.find((s) => {
var _a, _b, _c;
// Confirm namespace match first to avoid cross-namespace leakage
const sameNs = ((_b = (_a = s.metadata) === null || _a === void 0 ? void 0 : _a.namespace) !== null && _b !== void 0 ? _b : 'default') === ns;
// Confirm the Service name matches the governing service name
const sameName = ((_c = s.metadata) === null || _c === void 0 ? void 0 : _c.name) === svcName;
// Return true only when both namespace and name match
return sameNs && sameName;
});
// If found, include the governing Service (often headless: clusterIP: None)
if (svc) {
// Add the Service once, guarding against duplicates across strategies
this.addServiceIfNotSeen(output, seen, svc);
}
}
}
// Begin strategy (4): If Pod.spec.subdomain is set, include the same-named Service in the namespace
const subdomain = (_s = pod.spec) === null || _s === void 0 ? void 0 : _s.subdomain;
// Only attempt lookup when a subdomain is explicitly specified on the Pod
if (subdomain) {
// Locate a Service with the same name as the Pod subdomain in the same namespace
const svc = this.services.find((s) => {
var _a, _b, _c;
// Match on namespace equivalence to confine lookups properly
const sameNs = ((_b = (_a = s.metadata) === null || _a === void 0 ? void 0 : _a.namespace) !== null && _b !== void 0 ? _b : 'default') === ns;
// Match the Service name to the Pod subdomain (common with headless Services)
const sameName = ((_c = s.metadata) === null || _c === void 0 ? void 0 : _c.name) === subdomain;
// Return true only when both conditions are satisfied
return sameNs && sameName;
});
// If found, include the subdomain-matching Service
if (svc) {
// Add the Service once, guarding against duplicates across strategies
this.addServiceIfNotSeen(output, seen, svc);
}
}
// Return the aggregated, de-duplicated list of Services associated with the Pod
return output;
}
/*** Private Methods ***/
ensureUniqueNames(object) {
let name = object.resource.metadata.name;
let kind = object.resource.kind;
let kindToReplace;
let replacedWithKind;
// Handle lists
if (object.resource.hasOwnProperty('items')) {
const list = object.resource;
for (const item of list.items) {
this.ensureUniqueNames({ filePath: object.filePath, resource: item });
}
return;
}
// Process persistent volume claims of a K8s stateful workload
if (object.resource.kind == 'StatefulSet') {
const k8sWorkload = object.resource;
// Ensure that each persistent volume claim name is not creating a conflict
if (k8sWorkload.hasOwnProperty('spec') && k8sWorkload.spec.volumeClaimTemplates) {
// A hashset to ensure unique PVC names within the StatefulSet
const pvcNames = new Set();
// Iterate over all defined PVCs within the StatefulSet and catch duplicate names
for (const pvc of k8sWorkload.spec.volumeClaimTemplates) {
// Catch duplicate names
if (pvcNames.has(pvc.metadata.name)) {
throw new Error(`ERROR: Duplicate name '${pvc.metadata.name}' found in StatefulSet '${k8sWorkload.metadata.name}'. File path: ${object.filePath}`);
}
// Register the PVC name
pvcNames.add(pvc.metadata.name);
}
}
}
// Replace kinds with their equivalent
if (this.kindToEquivalent.has(kind)) {
kindToReplace = kind;
replacedWithKind = this.kindToEquivalent.get(kind);
kind = replacedWithKind;
}
// Register kind as a key
if (!this.kindToK8sObject[kind]) {
this.kindToK8sObject[kind] = {};
}
// Check if the name already exists for this kind, if so, throw an error accordingly
if (this.kindToK8sObject[kind][name]) {
const existingObject = this.kindToK8sObject[kind][name];
let fileMessage = object.filePath == existingObject.filePath
? `File path: ${object.filePath}`
: `File paths: '${object.filePath}' and '${existingObject.filePath}'`;
let message = `ERROR: Duplicate name '${name}' found for kind '${kind}'. File path: ${fileMessage}`;
// The following cases might occur:
// - ConfigMap == ConfigMap || Secret != Secret => A Secret already registered this name, so a ConfigMap cannot
// - Secret == Undefined || ConfigMap != Secret => A ConfigMap already registered this name, so a Secret cannot
if (existingObject.resource.kind == kindToReplace || existingObject.resource.kind != kind) {
message = `Error: A '${existingObject.resource.kind}' cannot share the same name as a '${kind}'. Name '${name}', ${fileMessage}.`;
}
throw new Error(message);
}
// Reserve this name for this kind
this.kindToK8sObject[kind][name] = object;
}
handleK8sStatefulWorkload(object) {
const k8sWorkload = object;
if (!k8sWorkload.hasOwnProperty('spec') || !k8sWorkload.spec.volumeClaimTemplates) {
return;
}
// Map persistent volume claim name to workload name
for (const pvc of k8sWorkload.spec.volumeClaimTemplates) {
this.pvcToWorkload.set(pvc.metadata.name, k8sWorkload.metadata.name);
}
}
handleHpa(filePath, hpa) {
// Handle Horizontal Pod Autoscaler
const hpaHandler = new hpa_1.K8sHorizontalPodAutoscalerHandler(filePath, hpa);
hpaHandler.handle();
// Only handle auto-scalable kinds
if (!types_1.K8sAutoscalableKinds.includes(hpa.spec.scaleTargetRef.kind)) {
return;
}
// Map the Horizontal Pod Autoscaler to workload
this.k8sWorkloadNameToHpa[hpa.spec.scaleTargetRef.name] = hpa;
}
handleEndpointSlice(es) {
var _a, _b, _c, _d;
// Resolve namespace with default
const ns = (_b = (_a = es.metadata) === null || _a === void 0 ? void 0 : _a.namespace) !== null && _b !== void 0 ? _b : 'default';
// Extract the well-known owner label
const svcName = (_d = (_c = es.metadata) === null || _c === void 0 ? void 0 : _c.labels) === null || _d === void 0 ? void 0 : _d['kubernetes.io/service-name'];
// Skip if the owner label is not present
if (!svcName) {
return;
}
// Compute composite key
const key = this.serviceKey(ns, svcName);
// Initialize key with a default value if it doesn't exist
if (!this.endpointSlicesByService.has(key)) {
this.endpointSlicesByService.set(key, []);
}
// Append the endpoint slice item to the map
this.endpointSlicesByService.get(key).push(es);
}
endpointSliceMatchesPod(es, ns, podName, podIP) {
var _a, _b, _c;
// Iterate endpoints (if any)
for (const ep of (_a = es.endpoints) !== null && _a !== void 0 ? _a : []) {
// Prefer explicit targetRef match
if (podName && ((_b = ep.targetRef) === null || _b === void 0 ? void 0 : _b.kind) === 'Pod' && ((_c = ep.targetRef.namespace) !== null && _c !== void 0 ? _c : ns) === ns && ep.targetRef.name === podName) {
return true;
}
// Fallback to addresses match using Pod IP
if (podIP && Array.isArray(ep.addresses) && ep.addresses.includes(podIP)) {
return true;
}
}
// No match found
return false;
}
addServiceIfNotSeen(out, seen, svc) {
var _a, _b, _c, _d;
const key = this.serviceKey((_b = (_a = svc.metadata) === null || _a === void 0 ? void 0 : _a.namespace) !== null && _b !== void 0 ? _b : 'default', (_d = (_c = svc.metadata) === null || _c === void 0 ? void 0 : _c.name) !== null && _d !== void 0 ? _d : '');
if (!seen.has(key)) {
seen.add(key);
out.push(svc);
}
}
isSelectorSubset(selector, labels) {
// Iterate over each selector key
for (const key of Object.keys(selector)) {
// Fail if any key is missing or mismatched
if (selector[key] !== labels[key]) {
// Return false on first mismatch
return false;
}
}
// Return true if all selector pairs matched
return true;
}
serviceKey(ns, name) {
return `${ns}/${name}`;
}
}
exports.K8sMapper = K8sMapper;
//# sourceMappingURL=mapper.js.map