diff --git a/api/kubernetes.go b/api/kubernetes.go index a363a0a63..45acea995 100644 --- a/api/kubernetes.go +++ b/api/kubernetes.go @@ -5,9 +5,8 @@ func KubernetesDefault() KubernetesData { Configuration: KubernetesConfiguration{ UseLoadBalancer: false, UseServerMetrics: false, - UseIngress: false, StorageClasses: []KubernetesStorageClassConfig{}, - IngressClasses: []string{}, + IngressClasses: []KubernetesIngressClassConfig{}, }, Snapshots: []KubernetesSnapshot{}, } diff --git a/api/portainer.go b/api/portainer.go index 8e03fb709..99ee05c25 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -332,9 +332,8 @@ type ( KubernetesConfiguration struct { UseLoadBalancer bool `json:"UseLoadBalancer"` UseServerMetrics bool `json:"UseServerMetrics"` - UseIngress bool `json:"UseIngress"` StorageClasses []KubernetesStorageClassConfig `json:"StorageClasses"` - IngressClasses []string `json:"IngressClasses"` + IngressClasses []KubernetesIngressClassConfig `json:"IngressClasses"` } // KubernetesStorageClassConfig represents a Kubernetes Storage Class configuration @@ -345,6 +344,12 @@ type ( AllowVolumeExpansion bool `json:"AllowVolumeExpansion"` } + // KubernetesIngressClassConfig represents a Kubernetes Ingress Class configuration + KubernetesIngressClassConfig struct { + Name string `json:"Name"` + Type string `json:"Type"` + } + // LDAPGroupSearchSettings represents settings used to search for groups in a LDAP server LDAPGroupSearchSettings struct { GroupBaseDN string `json:"GroupBaseDN"` diff --git a/app/kubernetes/converters/application.js b/app/kubernetes/converters/application.js index af1fe6b38..56cfdf2bb 100644 --- a/app/kubernetes/converters/application.js +++ b/app/kubernetes/converters/application.js @@ -7,6 +7,7 @@ import { KubernetesApplicationDataAccessPolicies, KubernetesApplicationDeploymentTypes, KubernetesApplicationPersistedFolder, + KubernetesApplicationPort, KubernetesApplicationPublishingTypes, KubernetesApplicationTypes, KubernetesPortainerApplicationNameLabel, @@ -25,7 +26,6 @@ import KubernetesStatefulSetConverter from 'Kubernetes/converters/statefulSet'; import KubernetesServiceConverter from 'Kubernetes/converters/service'; import KubernetesPersistentVolumeClaimConverter from 'Kubernetes/converters/persistentVolumeClaim'; import PortainerError from 'Portainer/error'; -import { KubernetesApplicationPort } from 'Kubernetes/models/application/models'; import { KubernetesIngressHelper } from 'Kubernetes/ingress/helper'; function _apiPortsToPublishedPorts(pList, pRefs) { @@ -270,9 +270,9 @@ class KubernetesApplicationConverter { const isIngress = _.filter(res.PublishedPorts, (p) => p.IngressName).length; if (app.ServiceType === KubernetesServiceTypes.LOAD_BALANCER) { res.PublishingType = KubernetesApplicationPublishingTypes.LOAD_BALANCER; - } else if (app.ServiceType === KubernetesServiceTypes.NODE_PORT && !isIngress) { + } else if (app.ServiceType === KubernetesServiceTypes.NODE_PORT) { res.PublishingType = KubernetesApplicationPublishingTypes.CLUSTER; - } else if (app.ServiceType === KubernetesServiceTypes.NODE_PORT && isIngress) { + } else if (app.ServiceType === KubernetesServiceTypes.CLUSTER_IP && isIngress) { res.PublishingType = KubernetesApplicationPublishingTypes.INGRESS; } else { res.PublishingType = KubernetesApplicationPublishingTypes.INTERNAL; diff --git a/app/kubernetes/converters/service.js b/app/kubernetes/converters/service.js index 44755f04b..6376bf3d6 100644 --- a/app/kubernetes/converters/service.js +++ b/app/kubernetes/converters/service.js @@ -3,12 +3,12 @@ import * as JsonPatch from 'fast-json-patch'; import { KubernetesServiceCreatePayload } from 'Kubernetes/models/service/payloads'; import { - KubernetesPortainerApplicationStackNameLabel, + KubernetesApplicationPublishingTypes, KubernetesPortainerApplicationNameLabel, KubernetesPortainerApplicationOwnerLabel, + KubernetesPortainerApplicationStackNameLabel, } from 'Kubernetes/models/application/models'; -import { KubernetesServiceHeadlessClusterIP, KubernetesService, KubernetesServicePort, KubernetesServiceTypes } from 'Kubernetes/models/service/models'; -import { KubernetesApplicationPublishingTypes } from 'Kubernetes/models/application/models'; +import { KubernetesService, KubernetesServiceHeadlessClusterIP, KubernetesServicePort, KubernetesServiceTypes } from 'Kubernetes/models/service/models'; import KubernetesServiceHelper from 'Kubernetes/helpers/serviceHelper'; function _publishedPortToServicePort(formValues, publishedPort, type) { @@ -42,7 +42,7 @@ class KubernetesServiceConverter { res.StackName = formValues.StackName ? formValues.StackName : formValues.Name; res.ApplicationOwner = formValues.ApplicationOwner; res.ApplicationName = formValues.Name; - if (formValues.PublishingType === KubernetesApplicationPublishingTypes.CLUSTER || formValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) { + if (formValues.PublishingType === KubernetesApplicationPublishingTypes.CLUSTER) { res.Type = KubernetesServiceTypes.NODE_PORT; } else if (formValues.PublishingType === KubernetesApplicationPublishingTypes.LOAD_BALANCER) { res.Type = KubernetesServiceTypes.LOAD_BALANCER; diff --git a/app/kubernetes/ingress/constants.js b/app/kubernetes/ingress/constants.js index 22af57a87..bd7a5cfea 100644 --- a/app/kubernetes/ingress/constants.js +++ b/app/kubernetes/ingress/constants.js @@ -1,4 +1,13 @@ export const KubernetesIngressClassAnnotation = 'kubernetes.io/ingress.class'; -export const KubernetesIngressClassMandatoryAnnotations = Object.freeze({ - nginx: { 'nginx.ingress.kubernetes.io/rewrite-target': '/$1' }, + +// keys must match KubernetesIngressClassTypes values to map them quickly using the ingress type +// KubernetesIngressClassRewriteTargetAnnotations[KubernetesIngressClassTypes.NGINX] for example +export const KubernetesIngressClassRewriteTargetAnnotations = Object.freeze({ + nginx: { 'nginx.ingress.kubernetes.io/rewrite-target': '/' }, + traefik: { 'traefik.ingress.kubernetes.io/rewrite-target': '/' }, +}); + +export const KubernetesIngressClassTypes = Object.freeze({ + NGINX: 'nginx', + TRAEFIK: 'traefik', }); diff --git a/app/kubernetes/ingress/converter.js b/app/kubernetes/ingress/converter.js index 311d22e08..966a61ca6 100644 --- a/app/kubernetes/ingress/converter.js +++ b/app/kubernetes/ingress/converter.js @@ -2,9 +2,10 @@ import * as _ from 'lodash-es'; import * as JsonPatch from 'fast-json-patch'; import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper'; -import { KubernetesIngressRule, KubernetesIngress } from './models'; +import { KubernetesResourcePoolIngressClassAnnotationFormValue, KubernetesResourcePoolIngressClassFormValue } from 'Kubernetes/models/resource-pool/formValues'; +import { KubernetesIngress, KubernetesIngressRule } from './models'; import { KubernetesIngressCreatePayload, KubernetesIngressRuleCreatePayload, KubernetesIngressRulePathCreatePayload } from './payloads'; -import { KubernetesIngressClassAnnotation, KubernetesIngressClassMandatoryAnnotations } from './constants'; +import { KubernetesIngressClassAnnotation, KubernetesIngressClassRewriteTargetAnnotations } from './constants'; export class KubernetesIngressConverter { // TODO: refactor @LP @@ -64,17 +65,68 @@ export class KubernetesIngressConverter { return ingresses; } + /** + * + * @param {KubernetesResourcePoolIngressClassFormValue} formValues + */ + static resourcePoolIngressClassFormValueToIngress(formValues) { + const res = new KubernetesIngress(); + res.Name = formValues.IngressClass.Name; + res.Namespace = formValues.Namespace; + const pairs = _.map(formValues.Annotations, (a) => [a.Key, a.Value]); + res.Annotations = _.fromPairs(pairs); + if (formValues.RewriteTarget) { + _.extend(res.Annotations, KubernetesIngressClassRewriteTargetAnnotations[formValues.IngressClass.Type]); + } + res.Annotations[KubernetesIngressClassAnnotation] = formValues.IngressClass.Name; + res.Host = formValues.Host; + return res; + } + + /** + * + * @param {KubernetesIngressClass} ics Ingress classes (saved in Portainer DB) + * @param {KubernetesIngress} ingresses Existing Kubernetes ingresses. Must be empty for RP CREATE VIEW and passed for RP EDIT VIEW + */ + static ingressClassesToFormValues(ics, ingresses) { + const res = _.map(ics, (ic) => { + const fv = new KubernetesResourcePoolIngressClassFormValue(); + fv.IngressClass = ic; + const ingress = _.find(ingresses, { Name: ic.Name }); + if (ingress) { + fv.Selected = true; + fv.WasSelected = true; + fv.Host = ingress.Host; + const [[rewriteKey]] = _.toPairs(KubernetesIngressClassRewriteTargetAnnotations[ic.Type]); + const annotations = _.map(_.toPairs(ingress.Annotations), ([key, value]) => { + if (key === rewriteKey) { + fv.RewriteTarget = true; + } else if (key !== KubernetesIngressClassAnnotation) { + const annotation = new KubernetesResourcePoolIngressClassAnnotationFormValue(); + annotation.Key = key; + annotation.Value = value; + return annotation; + } + }); + fv.Annotations = _.without(annotations, undefined); + fv.AdvancedConfig = fv.Annotations.length > 0; + } + return fv; + }); + return res; + } + 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); - } + res.metadata.annotations = data.Annotations; if (data.Paths && data.Paths.length) { + _.forEach(data.Paths, (p) => { + if (p.Host === 'undefined' || p.Host === undefined) { + p.Host = ''; + } + }); const groups = _.groupBy(data.Paths, 'Host'); const rules = _.map(groups, (paths, host) => { const rule = new KubernetesIngressRuleCreatePayload(); diff --git a/app/kubernetes/ingress/models.js b/app/kubernetes/ingress/models.js index e7181a48f..ea6ccd1a8 100644 --- a/app/kubernetes/ingress/models.js +++ b/app/kubernetes/ingress/models.js @@ -24,3 +24,12 @@ export function KubernetesIngressRule() { Path: '', }; } + +export function KubernetesIngressClass() { + return { + Name: '', + Type: undefined, + NeedsDeletion: false, + IsNew: true, + }; +} diff --git a/app/kubernetes/ingress/service.js b/app/kubernetes/ingress/service.js index c31f30585..d5c97d527 100644 --- a/app/kubernetes/ingress/service.js +++ b/app/kubernetes/ingress/service.js @@ -55,10 +55,10 @@ class KubernetesIngressService { /** * CREATE */ - async createAsync(formValues) { + async createAsync(ingress) { try { const params = {}; - const payload = KubernetesIngressConverter.createPayload(formValues); + const payload = KubernetesIngressConverter.createPayload(ingress); const namespace = payload.metadata.namespace; const data = await this.KubernetesIngresses(namespace).create(params, payload).$promise; return data; @@ -67,8 +67,8 @@ class KubernetesIngressService { } } - create(formValues) { - return this.$async(this.createAsync, formValues); + create(ingress) { + return this.$async(this.createAsync, ingress); } /** @@ -100,7 +100,7 @@ class KubernetesIngressService { async deleteAsync(ingress) { try { const params = new KubernetesCommonParams(); - params.id = ingress.Name; + params.id = ingress.IngressClass.Name; const namespace = ingress.Namespace; await this.KubernetesIngresses(namespace).delete(params).$promise; } catch (err) { diff --git a/app/kubernetes/models/application/formValues.js b/app/kubernetes/models/application/formValues.js index 0fcedf6b1..1f1df1197 100644 --- a/app/kubernetes/models/application/formValues.js +++ b/app/kubernetes/models/application/formValues.js @@ -151,7 +151,7 @@ export class KubernetesApplicationAutoScalerFormValue { } } -export function KubernetesApplicationFormValidationDuplicate() { +export function KubernetesFormValueDuplicate() { return { refs: {}, hasDuplicates: false, diff --git a/app/kubernetes/models/resource-pool/formValues.js b/app/kubernetes/models/resource-pool/formValues.js index f59446cee..9d2277f5d 100644 --- a/app/kubernetes/models/resource-pool/formValues.js +++ b/app/kubernetes/models/resource-pool/formValues.js @@ -8,15 +8,24 @@ export function KubernetesResourcePoolFormValues(defaults) { } /** - * @param {string} ingressClassName + * @param {KubernetesIngressClass} ingressClass */ -export function KubernetesResourcePoolIngressClassFormValue(ingressClassName) { +export function KubernetesResourcePoolIngressClassFormValue(ingressClass) { return { - Name: ingressClassName, - IngressClassName: ingressClassName, + Namespace: undefined, // will be filled inside ResourcePoolService.create + IngressClass: ingressClass, + RewriteTarget: false, + Annotations: [], // KubernetesResourcePoolIngressClassAnnotationFormValue Host: undefined, Selected: false, WasSelected: false, - Namespace: undefined, // will be filled inside ResourcePoolService.create + AdvancedConfig: false, + }; +} + +export function KubernetesResourcePoolIngressClassAnnotationFormValue() { + return { + Key: '', + Value: '', }; } diff --git a/app/kubernetes/services/resourcePoolService.js b/app/kubernetes/services/resourcePoolService.js index 7de236f21..4320bf750 100644 --- a/app/kubernetes/services/resourcePoolService.js +++ b/app/kubernetes/services/resourcePoolService.js @@ -6,6 +6,7 @@ import KubernetesResourcePoolConverter from 'Kubernetes/converters/resourcePool' import KubernetesResourceQuotaHelper from 'Kubernetes/helpers/resourceQuotaHelper'; import { KubernetesNamespace } from 'Kubernetes/models/namespace/models'; import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; +import { KubernetesIngressConverter } from 'Kubernetes/ingress/converter'; class KubernetesResourcePoolService { /* @ngInject */ @@ -89,7 +90,8 @@ class KubernetesResourcePoolService { const ingressPromises = _.map(formValues.IngressClasses, (c) => { if (c.Selected) { c.Namespace = namespace.Name; - return this.KubernetesIngressService.create(c); + const ingress = KubernetesIngressConverter.resourcePoolIngressClassFormValueToIngress(c); + return this.KubernetesIngressService.create(ingress); } }); await Promise.all(ingressPromises); diff --git a/app/kubernetes/views/applications/create/createApplication.html b/app/kubernetes/views/applications/create/createApplication.html index 8e9f58fa7..78e7b2674 100644 --- a/app/kubernetes/views/applications/create/createApplication.html +++ b/app/kubernetes/views/applications/create/createApplication.html @@ -1329,10 +1329,10 @@ class="form-control" name="ingress_route_{{ $index }}" ng-model="publishedPort.IngressRoute" - placeholder="foo" + placeholder="route" ng-required="!publishedPort.NeedsDeletion" ng-change="ctrl.onChangePortMappingIngressRoute()" - ng-pattern="/^\/?([a-zA-Z0-9]+[a-zA-Z0-9-/_]*[a-zA-Z0-9]|[a-zA-Z0-9]+)$/" + ng-pattern="/^(\/?[a-zA-Z0-9]+([a-zA-Z0-9-/_]*[a-zA-Z0-9])?|[a-zA-Z0-9]+)|(\/){1}$/" ng-disabled="ctrl.disableLoadBalancerEdit() || ctrl.isEditAndNotNewPublishedPort($index)" /> diff --git a/app/kubernetes/views/applications/create/createApplicationController.js b/app/kubernetes/views/applications/create/createApplicationController.js index b41711b41..01d250d04 100644 --- a/app/kubernetes/views/applications/create/createApplicationController.js +++ b/app/kubernetes/views/applications/create/createApplicationController.js @@ -20,7 +20,7 @@ import { KubernetesApplicationPersistedFolderFormValue, KubernetesApplicationPublishedPortFormValue, KubernetesApplicationPlacementFormValue, - KubernetesApplicationFormValidationDuplicate, + KubernetesFormValueDuplicate, } from 'Kubernetes/models/application/formValues'; import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper'; import KubernetesApplicationConverter from 'Kubernetes/converters/application'; @@ -367,14 +367,16 @@ class KubernetesCreateApplicationController { const publishedPort = this.formValues.PublishedPorts[index]; const ingress = _.find(this.filteredIngresses, { Name: publishedPort.IngressName }); publishedPort.IngressHost = ingress.Host; + this.onChangePublishedPorts(); } onChangePortMappingIngressRoute() { const state = this.state.duplicates.publishedPorts.ingressRoutes; + if (this.formValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) { - const newRoutes = _.map(this.formValues.PublishedPorts, (p) => (p.IsNew ? p.IngressRoute : undefined)); - const toDelRoutes = _.map(this.formValues.PublishedPorts, (p) => (p.NeedsDeletion ? p.IngressRoute : undefined)); - const allRoutes = _.flatMapDeep(this.ingresses, (c) => _.map(c.Paths, 'Path')); + const newRoutes = _.map(this.formValues.PublishedPorts, (p) => (p.IsNew && p.IngressRoute ? (p.IngressHost || p.IngressName) + p.IngressRoute : undefined)); + const toDelRoutes = _.map(this.formValues.PublishedPorts, (p) => (p.NeedsDeletion && p.IngressRoute ? (p.IngressHost || p.IngressName) + p.IngressRoute : undefined)); + const allRoutes = _.flatMap(this.ingresses, (i) => _.map(i.Paths, (p) => (p.Host || i.Name) + p.Path)); const duplicates = KubernetesFormValidationHelper.getDuplicates(newRoutes); _.forEach(newRoutes, (route, idx) => { if (_.includes(allRoutes, route) && !_.includes(toDelRoutes, route)) { @@ -814,7 +816,6 @@ class KubernetesCreateApplicationController { actionInProgress: false, useLoadBalancer: false, useServerMetrics: false, - canUseIngress: false, sliders: { cpu: { min: 0, @@ -834,17 +835,17 @@ class KubernetesCreateApplicationController { availableSizeUnits: ['MB', 'GB', 'TB'], alreadyExists: false, duplicates: { - environmentVariables: new KubernetesApplicationFormValidationDuplicate(), - persistedFolders: new KubernetesApplicationFormValidationDuplicate(), - configurationPaths: new KubernetesApplicationFormValidationDuplicate(), - existingVolumes: new KubernetesApplicationFormValidationDuplicate(), + environmentVariables: new KubernetesFormValueDuplicate(), + persistedFolders: new KubernetesFormValueDuplicate(), + configurationPaths: new KubernetesFormValueDuplicate(), + existingVolumes: new KubernetesFormValueDuplicate(), publishedPorts: { - containerPorts: new KubernetesApplicationFormValidationDuplicate(), - nodePorts: new KubernetesApplicationFormValidationDuplicate(), - ingressRoutes: new KubernetesApplicationFormValidationDuplicate(), - loadBalancerPorts: new KubernetesApplicationFormValidationDuplicate(), + containerPorts: new KubernetesFormValueDuplicate(), + nodePorts: new KubernetesFormValueDuplicate(), + ingressRoutes: new KubernetesFormValueDuplicate(), + loadBalancerPorts: new KubernetesFormValueDuplicate(), }, - placements: new KubernetesApplicationFormValidationDuplicate(), + placements: new KubernetesFormValueDuplicate(), }, isEdit: false, params: { diff --git a/app/kubernetes/views/applications/edit/application.html b/app/kubernetes/views/applications/edit/application.html index a19f2f015..197a3e5ea 100644 --- a/app/kubernetes/views/applications/edit/application.html +++ b/app/kubernetes/views/applications/edit/application.html @@ -247,7 +247,7 @@ -
{{ ctrl.application.ServiceName }}
@@ -261,6 +261,24 @@
+
+ This application is available for internal usage inside the cluster via the application name {{ ctrl.application.ServiceName }}
+ Copy
+
+
It can also be accessed via specific HTTP route(s).
+Refer to the below port configuration to access the application.
+Ingress controller | -
- Hostname
- |
-
-
-
- {{ class.Name }}
-
- |
- - - | -
+ + Enable and configure ingresses available to users when deploying applications. +
++ + This host is already used. +
++ + You can specify a list of annotations that will be associated to the ingress. +
+Ingress controller | -
- Hostname
- |
-
-
-
- {{ class.Name }}
-
- |
- - - | -
+ + Enable and configure ingresses available to users when deploying applications. +
++ + This host is already used. +
++ + You can specify a list of annotations that will be associated to the ingress. +
+