UNPKG

@controlplane/cli

Version:

Control Plane Corporation CLI

407 lines 21.4 kB
"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