@controlplane/cli
Version:
Control Plane Corporation CLI
284 lines • 13.5 kB
JavaScript
"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