mirror of
https://github.com/portainer/portainer.git
synced 2025-08-04 13:25:26 +02:00
feat(k8s/applications): expose applications via ingress (#4136)
* feat(k8s/endpoint): expose ingress controllers on endpoints * feat(k8s/applications): add ability to expose applications over ingress - missing RP and app edits * feat(k8s/application): add validation for ingress routes * feat(k8s/resource-pools): edit available ingress classes * fix(k8s/ingress): var name refactor was partially applied * feat(kubernetes): double validation on RP edit * feat(k8s/application): app edit ingress update + formvalidation + UI rework * feat(k8s/ingress): dictionary for default annotations on ingress creation * fix(k8s/application): temporary fix + TODO dev notice * feat(k8s/application): select default ingress of selected resource pool * feat(k8s/ingress): revert ingressClassName removal * feat(k8s/ingress): admins can now add an host to ingress in a resource pool * feat(k8s/resource-pool): list applications using RP ingresses * feat(k8s/configure): minor UI update * feat(k8s/configure): minor UI update * feat(k8s/configure): minor UI update * feat(k8s/configure): minor UI update * feat(k8s/configure): minor UI update * fix(k8s/ingresses): remove host if undefined * feat(k8s/resource-pool): remove the activate ingresses switch * fix(k8s/resource-pool): edditing an ingress host was deleting all the routes of the ingress * feat(k8s/application): prevent app deploy if no ports to publish and publishing type not internal * feat(k8s/ingress): minor UI update * fix(k8s/ingress): allow routes without prepending / * feat(k8s/application): add form validation on ingress route Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>
This commit is contained in:
parent
201c3ac143
commit
f91d3f1ca3
31 changed files with 1595 additions and 443 deletions
|
@ -1,19 +1,113 @@
|
|||
import * as _ from 'lodash-es';
|
||||
import { KubernetesIngressRule } from './models';
|
||||
import * as JsonPatch from 'fast-json-patch';
|
||||
|
||||
import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper';
|
||||
import { KubernetesIngressRule, KubernetesIngress } from './models';
|
||||
import { KubernetesIngressCreatePayload, KubernetesIngressRuleCreatePayload, KubernetesIngressRulePathCreatePayload } from './payloads';
|
||||
import { KubernetesIngressClassAnnotation, KubernetesIngressClassMandatoryAnnotations } from './constants';
|
||||
|
||||
export class KubernetesIngressConverter {
|
||||
// TODO: refactor @LP
|
||||
// currently only allows the first non-empty host to be used as the "configured" host.
|
||||
// As we currently only allow a single host to be used for a Portianer-managed ingress
|
||||
// it's working as the only non-empty host will be the one defined by the admin
|
||||
// but it will take a random existing host for non Portainer ingresses (CLI deployed)
|
||||
// Also won't support multiple hosts if we make it available in the future
|
||||
static apiToModel(data) {
|
||||
const rules = _.flatMap(data.spec.rules, (rule) => {
|
||||
return _.map(rule.http.paths, (path) => {
|
||||
const ingRule = new KubernetesIngressRule();
|
||||
ingRule.ServiceName = path.backend.serviceName;
|
||||
ingRule.Host = rule.host;
|
||||
ingRule.IP = data.status.loadBalancer.ingress ? data.status.loadBalancer.ingress[0].ip : undefined;
|
||||
ingRule.Port = path.backend.servicePort;
|
||||
ingRule.Path = path.path;
|
||||
return ingRule;
|
||||
});
|
||||
let host = undefined;
|
||||
const paths = _.flatMap(data.spec.rules, (rule) => {
|
||||
host = host || rule.host; // TODO: refactor @LP - read above
|
||||
return !rule.http
|
||||
? []
|
||||
: _.map(rule.http.paths, (path) => {
|
||||
const ingRule = new KubernetesIngressRule();
|
||||
ingRule.IngressName = data.metadata.name;
|
||||
ingRule.ServiceName = path.backend.serviceName;
|
||||
ingRule.Host = rule.host || '';
|
||||
ingRule.IP = data.status.loadBalancer.ingress ? data.status.loadBalancer.ingress[0].ip : undefined;
|
||||
ingRule.Port = path.backend.servicePort;
|
||||
ingRule.Path = path.path;
|
||||
return ingRule;
|
||||
});
|
||||
});
|
||||
return rules;
|
||||
|
||||
const res = new KubernetesIngress();
|
||||
res.Name = data.metadata.name;
|
||||
res.Namespace = data.metadata.namespace;
|
||||
res.Annotations = data.metadata.annotations || {};
|
||||
res.IngressClassName =
|
||||
data.metadata.annotations && data.metadata.annotations[KubernetesIngressClassAnnotation]
|
||||
? data.metadata.annotations[KubernetesIngressClassAnnotation]
|
||||
: data.spec.ingressClassName;
|
||||
res.Paths = paths;
|
||||
res.Host = host;
|
||||
return res;
|
||||
}
|
||||
|
||||
static applicationFormValuesToIngresses(formValues, serviceName) {
|
||||
const ingresses = angular.copy(formValues.OriginalIngresses);
|
||||
_.forEach(formValues.PublishedPorts, (p) => {
|
||||
const ingress = _.find(ingresses, { Name: p.IngressName });
|
||||
if (ingress && p.NeedsDeletion) {
|
||||
const path = _.find(ingress.Paths, { Port: p.ContainerPort, ServiceName: serviceName, Path: p.IngressRoute });
|
||||
_.remove(ingress.Paths, path);
|
||||
} else if (ingress && p.IsNew) {
|
||||
const rule = new KubernetesIngressRule();
|
||||
rule.IngressName = ingress.Name;
|
||||
rule.ServiceName = serviceName;
|
||||
rule.Port = p.ContainerPort;
|
||||
rule.Path = _.startsWith(p.IngressRoute, '/') ? p.IngressRoute : '/' + p.IngressRoute;
|
||||
rule.Host = p.IngressHost;
|
||||
ingress.Paths.push(rule);
|
||||
}
|
||||
});
|
||||
return ingresses;
|
||||
}
|
||||
|
||||
static createPayload(data) {
|
||||
const res = new KubernetesIngressCreatePayload();
|
||||
res.metadata.name = data.Name;
|
||||
res.metadata.namespace = data.Namespace;
|
||||
res.metadata.annotations = data.Annotations || {};
|
||||
res.metadata.annotations[KubernetesIngressClassAnnotation] = data.IngressClassName;
|
||||
const annotations = KubernetesIngressClassMandatoryAnnotations[data.Name];
|
||||
if (annotations) {
|
||||
_.extend(res.metadata.annotations, annotations);
|
||||
}
|
||||
if (data.Paths && data.Paths.length) {
|
||||
const groups = _.groupBy(data.Paths, 'Host');
|
||||
const rules = _.map(groups, (paths, host) => {
|
||||
const rule = new KubernetesIngressRuleCreatePayload();
|
||||
|
||||
if (host === 'undefined' || _.isEmpty(host)) {
|
||||
host = data.Host;
|
||||
}
|
||||
if (host === data.PreviousHost && host !== data.Host) {
|
||||
host = data.Host;
|
||||
}
|
||||
KubernetesCommonHelper.assignOrDeleteIfEmpty(rule, 'host', host);
|
||||
rule.http.paths = _.map(paths, (p) => {
|
||||
const path = new KubernetesIngressRulePathCreatePayload();
|
||||
path.path = p.Path;
|
||||
path.backend.serviceName = p.ServiceName;
|
||||
path.backend.servicePort = p.Port;
|
||||
return path;
|
||||
});
|
||||
return rule;
|
||||
});
|
||||
KubernetesCommonHelper.assignOrDeleteIfEmpty(res, 'spec.rules', rules);
|
||||
} else if (data.Host) {
|
||||
res.spec.rules = [{ host: data.Host }];
|
||||
} else {
|
||||
delete res.spec.rules;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
static patchPayload(oldData, newData) {
|
||||
const oldPayload = KubernetesIngressConverter.createPayload(oldData);
|
||||
const newPayload = KubernetesIngressConverter.createPayload(newData);
|
||||
const payload = JsonPatch.compare(oldPayload, newPayload);
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue