UNPKG

@controlplane/cli

Version:

Control Plane Corporation CLI

284 lines 13.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.K8sIngressHandler = void 0; const helper_1 = require("../../util/helper"); class K8sIngressHandler { constructor(filePath, k8sIngress, mapper) { this.filePath = filePath; this.k8sIngress = k8sIngress; this.mapper = mapper; this.domains = []; } /*** Public Methods ***/ handle() { // Validate K8s Ingress this.validate(); // Process rules and create domains this.processRules(); // Warn if any domain requires dedicated load balancer this.warnIfDedicatedLoadBalancerRequired(); // Return the accumulated domains return this.domains; } /*** Private Methods ***/ processRules() { // Group rules by resolved domain name (after stripping wildcard prefix) const domainNameToDomain = new Map(); for (const rule of this.k8sIngress.spec.rules) { const host = rule.host; const isWildcard = host.startsWith('*.'); const domainName = isWildcard ? host.substring(2) : host; // Get or create domain for this domain name let domain = domainNameToDomain.get(domainName); // If not found in the map, create it if (!domain) { domain = this.createDomainForHost(host); domainNameToDomain.set(domainName, domain); } else if (isWildcard) { // If domain exists and this is a wildcard, enable acceptAllSubdomains domain.spec.acceptAllSubdomains = true; } // Process the rule's paths and add routes to the domain this.processRule(domain, rule); } // Add all domains to the result for (const domain of domainNameToDomain.values()) { this.domains.push(domain); } } createDomainForHost(host) { // Check for wildcard host const isWildcard = host.startsWith('*.'); const domainName = isWildcard ? host.substring(2) : host; const domain = { kind: 'domain', name: domainName, description: `Converted from K8s Ingress: ${this.k8sIngress.metadata.name}`, tags: this.k8sIngress.metadata.labels, spec: { dnsMode: 'cname', certChallengeType: 'http01', ports: [ { number: 443, protocol: 'http2', routes: [], }, ], }, }; // Handle wildcard host if (isWildcard) { domain.spec.acceptAllSubdomains = true; } return domain; } processRule(domain, rule) { // Skip if no http paths if (!rule.http || !rule.http.paths || rule.http.paths.length === 0) { return; } // Process each path for (const pathEntry of rule.http.paths) { const route = this.processPath(pathEntry); this.addRouteToDomain(domain, route); } } processPath(pathEntry) { var _a, _b, _c; // Get service (existence validated in validate()) const serviceName = pathEntry.backend.service.name; const service = this.mapper.services.find((s) => { var _a; return ((_a = s.metadata) === null || _a === void 0 ? void 0 : _a.name) === serviceName; }); // Resolve service to workload link const workloadLink = this.resolveServiceToWorkloadLink(service); // Convert path based on pathType const routePathConfig = this.convertPathToRouteConfig(pathEntry); const route = { ...routePathConfig, workloadLink: workloadLink, }; // Resolve container port from service port if ((_c = (_b = (_a = pathEntry.backend) === null || _a === void 0 ? void 0 : _a.service) === null || _b === void 0 ? void 0 : _b.port) === null || _c === void 0 ? void 0 : _c.number) { const containerPort = this.resolveContainerPort(service, pathEntry.backend.service.port.number); if (containerPort) { route.port = containerPort; } } return route; } resolveContainerPort(service, servicePort) { var _a, _b, _c, _d, _e, _f, _g, _h; // Find the service port entry that matches the ingress backend port const servicePortEntry = (_b = (_a = service.spec) === null || _a === void 0 ? void 0 : _a.ports) === null || _b === void 0 ? void 0 : _b.find((p) => p.port === servicePort); if (!servicePortEntry) { return undefined; } // Get targetPort (defaults to port if not specified) const targetPort = (_c = servicePortEntry.targetPort) !== null && _c !== void 0 ? _c : servicePortEntry.port; if (targetPort === undefined) { return undefined; } // If targetPort is a number, return it directly if (typeof targetPort === 'number') { return targetPort; } // If targetPort is a string (port name), resolve it from the workload container const workload = this.mapper.workloads.find((w) => { var _a, _b, _c; const podLabels = (_c = (_b = (_a = w.spec) === null || _a === void 0 ? void 0 : _a.template) === null || _b === void 0 ? void 0 : _b.metadata) === null || _c === void 0 ? void 0 : _c.labels; return podLabels && this.mapper.isSelectorSubset(service.spec.selector, podLabels); }); if (!workload) { return undefined; } // Search containers for a port with matching name for (const container of (_g = (_f = (_e = (_d = workload.spec) === null || _d === void 0 ? void 0 : _d.template) === null || _e === void 0 ? void 0 : _e.spec) === null || _f === void 0 ? void 0 : _f.containers) !== null && _g !== void 0 ? _g : []) { for (const port of (_h = container.ports) !== null && _h !== void 0 ? _h : []) { if (port.name === targetPort) { return port.containerPort; } } } return undefined; } resolveServiceToWorkloadLink(service) { // Get GVC from context or use placeholder const gvc = this.mapper.ctx.gvc || '{{GVC}}'; // Infer workload name from service const workloadName = this.inferWorkloadNameFromService(service); // Construct the workload link and return it return `//gvc/${gvc}/workload/${workloadName}`; } inferWorkloadNameFromService(service) { // Validation guarantees: service has selector and a matching workload exists const selector = service.spec.selector; // Find the workload whose pod template labels match the service selector const workload = this.mapper.workloads.find((w) => { var _a, _b, _c; const podLabels = (_c = (_b = (_a = w.spec) === null || _a === void 0 ? void 0 : _a.template) === null || _b === void 0 ? void 0 : _b.metadata) === null || _c === void 0 ? void 0 : _c.labels; return podLabels && this.mapper.isSelectorSubset(selector, podLabels); }); // Return the workload name that the service is pointing to return workload.metadata.name; } convertPathToRouteConfig(pathEntry) { const path = pathEntry.path || '/'; const pathType = pathEntry.pathType || 'Prefix'; switch (pathType) { case 'Exact': // For Exact paths, use regex with anchors return { regex: this.escapeRegex(path) }; case 'Prefix': case 'ImplementationSpecific': default: // For Prefix and ImplementationSpecific, use prefix return { prefix: path }; } } escapeRegex(path) { // Escape regex special characters and add anchors for exact matching const escaped = path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); return `^${escaped}$`; } addRouteToDomain(domain, route) { const routes = domain.spec.ports[0].routes; // Check if an identical route already exists (deduplicate) const isDuplicate = routes.some((existingRoute) => this.areRoutesIdentical(existingRoute, route)); // Only add the route if it's not a duplicate if (!isDuplicate) { routes.push(route); } } areRoutesIdentical(a, b) { // Compare path configuration (prefix or regex) if (a.prefix !== b.prefix) { return false; } if (a.regex !== b.regex) { return false; } // Compare workload link if (a.workloadLink !== b.workloadLink) { return false; } // Compare port if (a.port !== b.port) { return false; } return true; } warnIfDedicatedLoadBalancerRequired() { const requiresDedicatedLB = this.domains.some((domain) => { var _a, _b; return ((_a = domain.spec) === null || _a === void 0 ? void 0 : _a.acceptAllSubdomains) === true || ((_b = domain.spec) === null || _b === void 0 ? void 0 : _b.acceptAllHosts) === true; }); if (requiresDedicatedLB) { this.mapper.warnings.push('Domains with "acceptAllSubdomains" or "acceptAllHosts" enabled require a Dedicated Load Balancer on the target GVC.'); } } // Validators // validate() { var _a, _b, _c; if (!this.k8sIngress.spec) { this.ensurePropertyPresence('spec'); } const spec = this.k8sIngress.spec; if (!spec.rules) { this.ensurePropertyPresence('spec.rules'); } const rules = spec.rules; if (!Array.isArray(rules)) { this.raiseCustomK8sError(`'spec.rules' must be an array`); } if (rules.length === 0) { this.raiseCustomK8sError(`'spec.rules' must have at least one rule with a host`); } // Validate each rule has a host and at least one path with a backend for (let ruleIndex = 0; ruleIndex < rules.length; ruleIndex++) { const rule = rules[ruleIndex]; if (!rule.host) { this.ensurePropertyPresence(`spec.rules[${ruleIndex}].host`); } if (!rule.http) { this.ensurePropertyPresence(`spec.rules[${ruleIndex}].http`); } if (!rule.http.paths || rule.http.paths.length === 0) { this.ensurePropertyPresence(`spec.rules[${ruleIndex}].http.paths`); } // Validate each path has a backend with service name that exists in mapper for (let pathIndex = 0; pathIndex < rule.http.paths.length; pathIndex++) { const path = rule.http.paths[pathIndex]; if (!((_b = (_a = path.backend) === null || _a === void 0 ? void 0 : _a.service) === null || _b === void 0 ? void 0 : _b.name)) { this.ensurePropertyPresence(`spec.rules[${ruleIndex}].http.paths[${pathIndex}].backend.service.name`); } const serviceName = path.backend.service.name; const service = this.mapper.services.find((s) => { var _a; return ((_a = s.metadata) === null || _a === void 0 ? void 0 : _a.name) === serviceName; }); if (!service) { this.raiseCustomK8sError(`Service '${serviceName}' referenced in spec.rules[${ruleIndex}].http.paths[${pathIndex}].backend.service.name was not found`); } // Validate service has a selector if (!((_c = service.spec) === null || _c === void 0 ? void 0 : _c.selector)) { this.raiseCustomK8sError(`Service '${serviceName}' referenced in spec.rules[${ruleIndex}].http.paths[${pathIndex}].backend.service.name must have a selector defined`); } // Validate there is a workload matching this service selector const selector = service.spec.selector; const matchingWorkload = this.mapper.workloads.find((workload) => { var _a, _b, _c; const podLabels = (_c = (_b = (_a = workload.spec) === null || _a === void 0 ? void 0 : _a.template) === null || _b === void 0 ? void 0 : _b.metadata) === null || _c === void 0 ? void 0 : _c.labels; return podLabels && this.mapper.isSelectorSubset(selector, podLabels); }); if (!matchingWorkload) { this.raiseCustomK8sError(`No workload found matching the selector of service '${serviceName}' referenced in spec.rules[${ruleIndex}].http.paths[${pathIndex}].backend.service.name. ` + `Ensure a Deployment, StatefulSet, DaemonSet, ReplicaSet, or ReplicationController with matching pod template labels is included in the conversion`); } } } } // Validation Helpers // ensurePropertyPresence(property) { (0, helper_1.ensurePropertyPresence)(property, this.filePath, this.k8sIngress); } raiseCustomK8sError(message) { (0, helper_1.raiseCustomK8sError)(message, this.filePath, this.k8sIngress); } } exports.K8sIngressHandler = K8sIngressHandler; //# sourceMappingURL=ingress.js.map