diff --git a/api/database/models/ingress.go b/api/database/models/ingress.go index 393ed0363..8e4f59886 100644 --- a/api/database/models/ingress.go +++ b/api/database/models/ingress.go @@ -59,9 +59,6 @@ func (r K8sIngressInfo) Validate(request *http.Request) error { if r.Namespace == "" { return errors.New("missing ingress Namespace from the request payload") } - if r.ClassName == "" { - return errors.New("missing ingress ClassName from the request payload") - } return nil } diff --git a/api/datastore/migrate_data_test.go b/api/datastore/migrate_data_test.go index a1b52e860..41af3c43a 100644 --- a/api/datastore/migrate_data_test.go +++ b/api/datastore/migrate_data_test.go @@ -275,7 +275,7 @@ func migrateDBTestHelper(t *testing.T, srcPath, wantPath string) error { // Compare the result we got with the one we wanted. if diff := cmp.Diff(wantJSON, gotJSON); diff != "" { - gotPath := filepath.Join(t.TempDir(), "portainer-migrator-test-fail.json") + gotPath := filepath.Join(os.TempDir(), "portainer-migrator-test-fail.json") os.WriteFile( gotPath, gotJSON, diff --git a/api/datastore/migrator/migrate_dbversion70.go b/api/datastore/migrator/migrate_dbversion70.go index ff121f598..5aadff56c 100644 --- a/api/datastore/migrator/migrate_dbversion70.go +++ b/api/datastore/migrator/migrate_dbversion70.go @@ -8,7 +8,7 @@ import ( func (m *Migrator) migrateDBVersionToDB70() error { log.Info().Msg("- add IngressAvailabilityPerNamespace field") - if err := m.addIngressAvailabilityPerNamespaceFieldDB70(); err != nil { + if err := m.updateIngressFieldsForEnvDB70(); err != nil { return err } @@ -51,7 +51,7 @@ func (m *Migrator) migrateDBVersionToDB70() error { return nil } -func (m *Migrator) addIngressAvailabilityPerNamespaceFieldDB70() error { +func (m *Migrator) updateIngressFieldsForEnvDB70() error { endpoints, err := m.endpointService.Endpoints() if err != nil { return err @@ -59,6 +59,7 @@ func (m *Migrator) addIngressAvailabilityPerNamespaceFieldDB70() error { for _, endpoint := range endpoints { endpoint.Kubernetes.Configuration.IngressAvailabilityPerNamespace = true + endpoint.Kubernetes.Configuration.AllowNoneIngressClass = false err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint) if err != nil { diff --git a/api/datastore/test_data/output_24_to_latest.json b/api/datastore/test_data/output_24_to_latest.json index 99f22dea1..d21359eb6 100644 --- a/api/datastore/test_data/output_24_to_latest.json +++ b/api/datastore/test_data/output_24_to_latest.json @@ -52,6 +52,7 @@ "IsEdgeDevice": false, "Kubernetes": { "Configuration": { + "AllowNoneIngressClass": false, "EnableResourceOverCommit": false, "IngressAvailabilityPerNamespace": true, "IngressClasses": null, diff --git a/api/http/handler/kubernetes/ingresses.go b/api/http/handler/kubernetes/ingresses.go index abc9842ed..8af98df90 100644 --- a/api/http/handler/kubernetes/ingresses.go +++ b/api/http/handler/kubernetes/ingresses.go @@ -57,11 +57,22 @@ func (handler *Handler) getKubernetesIngressControllers(w http.ResponseWriter, r err, ) } + + // Add none controller if "AllowNone" is set for endpoint. + if endpoint.Kubernetes.Configuration.AllowNoneIngressClass { + controllers = append(controllers, models.K8sIngressController{ + Name: "none", + ClassName: "none", + Type: "custom", + }) + } existingClasses := endpoint.Kubernetes.Configuration.IngressClasses var updatedClasses []portainer.KubernetesIngressClassConfig for i := range controllers { controllers[i].Availability = true - controllers[i].New = true + if controllers[i].ClassName != "none" { + controllers[i].New = true + } var updatedClass portainer.KubernetesIngressClassConfig updatedClass.Name = controllers[i].ClassName @@ -153,6 +164,14 @@ func (handler *Handler) getKubernetesIngressControllersByNamespace(w http.Respon err, ) } + // Add none controller if "AllowNone" is set for endpoint. + if endpoint.Kubernetes.Configuration.AllowNoneIngressClass { + currentControllers = append(currentControllers, models.K8sIngressController{ + Name: "none", + ClassName: "none", + Type: "custom", + }) + } kubernetesConfig := endpoint.Kubernetes.Configuration existingClasses := kubernetesConfig.IngressClasses ingressAvailabilityPerNamespace := kubernetesConfig.IngressAvailabilityPerNamespace @@ -161,7 +180,9 @@ func (handler *Handler) getKubernetesIngressControllersByNamespace(w http.Respon for i := range currentControllers { var globallyblocked bool currentControllers[i].Availability = true - currentControllers[i].New = true + if currentControllers[i].ClassName != "none" { + currentControllers[i].New = true + } var updatedClass portainer.KubernetesIngressClassConfig updatedClass.Name = currentControllers[i].ClassName @@ -258,6 +279,14 @@ func (handler *Handler) updateKubernetesIngressControllers(w http.ResponseWriter err, ) } + // Add none controller if "AllowNone" is set for endpoint. + if endpoint.Kubernetes.Configuration.AllowNoneIngressClass { + controllers = append(controllers, models.K8sIngressController{ + Name: "none", + ClassName: "none", + Type: "custom", + }) + } var updatedClasses []portainer.KubernetesIngressClassConfig for i := range controllers { controllers[i].Availability = true diff --git a/api/kubernetes/cli/ingress.go b/api/kubernetes/cli/ingress.go index 7eaf3e593..cc46038bb 100644 --- a/api/kubernetes/cli/ingress.go +++ b/api/kubernetes/cli/ingress.go @@ -90,11 +90,11 @@ func (kcl *KubeClient) GetIngresses(namespace string) ([]models.K8sIngressInfo, var infos []models.K8sIngressInfo for _, ingress := range ingressList.Items { - ingressClass := ingress.Spec.IngressClassName var info models.K8sIngressInfo info.Name = ingress.Name info.UID = string(ingress.UID) info.Namespace = namespace + ingressClass := ingress.Spec.IngressClassName info.ClassName = "" if ingressClass != nil { info.ClassName = *ingressClass @@ -113,6 +113,10 @@ func (kcl *KubeClient) GetIngresses(namespace string) ([]models.K8sIngressInfo, // Gather list of paths and hosts. hosts := make(map[string]struct{}) for _, r := range ingress.Spec.Rules { + // We collect all exiting hosts in a map to avoid duplicates. + // Then, later convert it to a slice for the frontend. + hosts[r.Host] = struct{}{} + if r.HTTP == nil { continue } @@ -124,12 +128,10 @@ func (kcl *KubeClient) GetIngresses(namespace string) ([]models.K8sIngressInfo, path.IngressName = info.Name path.Host = r.Host - // We collect all exiting hosts in a map to avoid duplicates. - // Then, later convert it to a slice for the frontend. - hosts[r.Host] = struct{}{} - path.Path = p.Path - path.PathType = string(*p.PathType) + if p.PathType != nil { + path.PathType = string(*p.PathType) + } path.ServiceName = p.Backend.Service.Name path.Port = int(p.Backend.Service.Port.Number) info.Paths = append(info.Paths, path) @@ -154,7 +156,9 @@ func (kcl *KubeClient) CreateIngress(namespace string, info models.K8sIngressInf ingress.Name = info.Name ingress.Namespace = info.Namespace - ingress.Spec.IngressClassName = &info.ClassName + if info.ClassName != "" { + ingress.Spec.IngressClassName = &info.ClassName + } ingress.Annotations = info.Annotations // Store TLS information. @@ -224,7 +228,9 @@ func (kcl *KubeClient) UpdateIngress(namespace string, info models.K8sIngressInf ingress.Name = info.Name ingress.Namespace = info.Namespace - ingress.Spec.IngressClassName = &info.ClassName + if info.ClassName != "" { + ingress.Spec.IngressClassName = &info.ClassName + } ingress.Annotations = info.Annotations // Store TLS information. diff --git a/api/portainer.go b/api/portainer.go index d1df3c2be..4fbd3d61c 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -555,6 +555,7 @@ type ( IngressClasses []KubernetesIngressClassConfig `json:"IngressClasses"` RestrictDefaultNamespace bool `json:"RestrictDefaultNamespace"` IngressAvailabilityPerNamespace bool `json:"IngressAvailabilityPerNamespace"` + AllowNoneIngressClass bool `json:"AllowNoneIngressClass"` } // KubernetesStorageClassConfig represents a Kubernetes Storage Class configuration diff --git a/app/kubernetes/react/components/index.ts b/app/kubernetes/react/components/index.ts index bf0613ea2..b4a59cfb4 100644 --- a/app/kubernetes/react/components/index.ts +++ b/app/kubernetes/react/components/index.ts @@ -12,9 +12,10 @@ export const componentsModule = angular .component( 'ingressClassDatatable', r2a(IngressClassDatatable, [ - 'onChangeAvailability', + 'onChangeControllers', 'description', 'ingressControllers', + 'allowNoneIngressClass', 'isLoading', 'noIngressControllerLabel', 'view', diff --git a/app/kubernetes/react/views/networks/ingresses/CreateIngressView/CreateIngressView.tsx b/app/kubernetes/react/views/networks/ingresses/CreateIngressView/CreateIngressView.tsx index a8d715936..23ee4aca7 100644 --- a/app/kubernetes/react/views/networks/ingresses/CreateIngressView/CreateIngressView.tsx +++ b/app/kubernetes/react/views/networks/ingresses/CreateIngressView/CreateIngressView.tsx @@ -157,7 +157,9 @@ export function CreateIngressView() { const existingIngressClass = useMemo( () => ingressControllersResults.data?.find( - (i) => i.ClassName === ingressRule.IngressClassName + (i) => + i.ClassName === ingressRule.IngressClassName || + (i.Type === 'custom' && ingressRule.IngressClassName === '') ), [ingressControllersResults.data, ingressRule.IngressClassName] ); @@ -177,10 +179,11 @@ export function CreateIngressView() { ingressRule.IngressClassName && !ingressControllersResults.isLoading ) { + const optionLabel = !ingressRule.IngressType + ? `${ingressRule.IngressClassName} - NOT FOUND` + : `${ingressRule.IngressClassName} - DISALLOWED`; ingressClassOptions.push({ - label: !ingressRule.IngressType - ? `${ingressRule.IngressClassName} - NOT FOUND` - : `${ingressRule.IngressClassName} - DISALLOWED`, + label: optionLabel, value: ingressRule.IngressClassName, }); } @@ -206,6 +209,7 @@ export function CreateIngressView() { !!params.name && ingressesResults.data && !ingressRule.IngressName && + !ingressControllersResults.isLoading && !ingressControllersResults.isLoading ) { // if it is an edit screen, prepare the rule from the ingress @@ -214,9 +218,11 @@ export function CreateIngressView() { ); if (ing) { const type = ingressControllersResults.data?.find( - (c) => c.ClassName === ing.ClassName + (c) => + c.ClassName === ing.ClassName || + (c.Type === 'custom' && !ing.ClassName) )?.Type; - const r = prepareRuleFromIngress(ing); + const r = prepareRuleFromIngress(ing, type); r.IngressType = type || r.IngressType; setIngressRule(r); } @@ -636,7 +642,7 @@ export function CreateIngressView() { setIngressRule(rule); } - function addNewAnnotation(type?: 'rewrite' | 'regex') { + function addNewAnnotation(type?: 'rewrite' | 'regex' | 'ingressClass') { const rule = { ...ingressRule }; const annotation: Annotation = { @@ -644,13 +650,21 @@ export function CreateIngressView() { Value: '', ID: uuidv4(), }; - if (type === 'rewrite') { - annotation.Key = 'nginx.ingress.kubernetes.io/rewrite-target'; - annotation.Value = '/$1'; - } - if (type === 'regex') { - annotation.Key = 'nginx.ingress.kubernetes.io/use-regex'; - annotation.Value = 'true'; + switch (type) { + case 'rewrite': + annotation.Key = 'nginx.ingress.kubernetes.io/rewrite-target'; + annotation.Value = '/$1'; + break; + case 'regex': + annotation.Key = 'nginx.ingress.kubernetes.io/use-regex'; + annotation.Value = 'true'; + break; + case 'ingressClass': + annotation.Key = 'kubernetes.io/ingress.class'; + annotation.Value = ''; + break; + default: + break; } rule.Annotations = rule.Annotations || []; rule.Annotations?.push(annotation); @@ -690,10 +704,13 @@ export function CreateIngressView() { function handleCreateIngressRules() { const rule = { ...ingressRule }; + const classNameToSend = + rule.IngressClassName === 'none' ? '' : rule.IngressClassName; + const ingress: Ingress = { Namespace: namespace, Name: rule.IngressName, - ClassName: rule.IngressClassName, + ClassName: classNameToSend, Hosts: rule.Hosts.map((host) => host.Host), Paths: preparePaths(rule.IngressName, rule.Hosts), TLS: prepareTLS(rule.Hosts), diff --git a/app/kubernetes/react/views/networks/ingresses/CreateIngressView/IngressForm.tsx b/app/kubernetes/react/views/networks/ingresses/CreateIngressView/IngressForm.tsx index 7e22d7c61..87e49c645 100644 --- a/app/kubernetes/react/views/networks/ingresses/CreateIngressView/IngressForm.tsx +++ b/app/kubernetes/react/views/networks/ingresses/CreateIngressView/IngressForm.tsx @@ -47,7 +47,7 @@ interface Props { addNewIngressHost: (noHost?: boolean) => void; addNewIngressRoute: (hostIndex: number) => void; - addNewAnnotation: (type?: 'rewrite' | 'regex') => void; + addNewAnnotation: (type?: 'rewrite' | 'regex' | 'ingressClass') => void; handleNamespaceChange: (val: string) => void; handleHostChange: (hostIndex: number, val: string) => void; @@ -249,9 +249,10 @@ export function IngressForm({ onClick={() => addNewAnnotation('rewrite')} icon={Plus} title="When the exposed URLs for your applications differ from the specified paths in the ingress, use the rewrite target annotation to denote the path to redirect to." + data-cy="add-rewrite-annotation" > {' '} - add rewrite annotation + Add rewrite annotation )} + + {rule.IngressType === 'custom' && ( + + )}
Rules
diff --git a/app/kubernetes/react/views/networks/ingresses/CreateIngressView/utils.ts b/app/kubernetes/react/views/networks/ingresses/CreateIngressView/utils.ts index 53adeaa20..79204e78e 100644 --- a/app/kubernetes/react/views/networks/ingresses/CreateIngressView/utils.ts +++ b/app/kubernetes/react/views/networks/ingresses/CreateIngressView/utils.ts @@ -1,6 +1,7 @@ import { v4 as uuidv4 } from 'uuid'; import { Annotation } from '@/kubernetes/react/views/networks/ingresses/components/annotations/types'; +import { SupportedIngControllerTypes } from '@/react/kubernetes/cluster/ingressClass/types'; import { TLS, Ingress } from '../types'; @@ -62,7 +63,7 @@ export function prepareRuleHostsFromIngress(ing: Ingress) { h.Host = host; h.Secret = getSecretByHost(host, ing.TLS); h.Paths = []; - ing.Paths.forEach((path) => { + ing.Paths?.forEach((path) => { if (path.Host === host) { h.Paths.push({ Route: path.Path, @@ -99,12 +100,15 @@ export function getAnnotationsForEdit( return result; } -export function prepareRuleFromIngress(ing: Ingress): Rule { +export function prepareRuleFromIngress( + ing: Ingress, + type?: SupportedIngControllerTypes +): Rule { return { Key: uuidv4(), IngressName: ing.Name, Namespace: ing.Namespace, - IngressClassName: ing.ClassName, + IngressClassName: type === 'custom' ? 'none' : ing.ClassName, Hosts: prepareRuleHostsFromIngress(ing) || [], Annotations: ing.Annotations ? getAnnotationsForEdit(ing.Annotations) : [], IngressType: ing.Type, diff --git a/app/kubernetes/react/views/networks/ingresses/queries.ts b/app/kubernetes/react/views/networks/ingresses/queries.ts index 5c0f882f6..f46551770 100644 --- a/app/kubernetes/react/views/networks/ingresses/queries.ts +++ b/app/kubernetes/react/views/networks/ingresses/queries.ts @@ -96,8 +96,11 @@ export function useIngresses( const serviceNamesInNamespace = servicesInNamespace?.map( (service) => service.Name ); - ing.Paths.forEach((path, pIndex) => { - if (!serviceNamesInNamespace?.includes(path.ServiceName)) { + ing.Paths?.forEach((path, pIndex) => { + if ( + !serviceNamesInNamespace?.includes(path.ServiceName) && + filteredIngresses[iIndex].Paths + ) { filteredIngresses[iIndex].Paths[pIndex].HasService = false; } else { filteredIngresses[iIndex].Paths[pIndex].HasService = true; @@ -186,6 +189,7 @@ export function useIngressControllers( }, { enabled: !!namespace, + cacheTime: 0, ...withError('Unable to get ingress controllers'), } ); diff --git a/app/kubernetes/react/views/networks/ingresses/types.ts b/app/kubernetes/react/views/networks/ingresses/types.ts index ac0ca30f6..f96de587f 100644 --- a/app/kubernetes/react/views/networks/ingresses/types.ts +++ b/app/kubernetes/react/views/networks/ingresses/types.ts @@ -2,6 +2,7 @@ import { PaginationTableSettings, SortableTableSettings, } from '@/react/components/datatables/types'; +import { SupportedIngControllerTypes } from '@/react/kubernetes/cluster/ingressClass/types'; export interface TableSettings extends SortableTableSettings, @@ -42,6 +43,6 @@ export interface IngressController { Name: string; ClassName: string; Availability: string; - Type: string; + Type: SupportedIngControllerTypes; New: boolean; } diff --git a/app/kubernetes/views/configure/configure.html b/app/kubernetes/views/configure/configure.html index 879a034cf..29f9bdad3 100644 --- a/app/kubernetes/views/configure/configure.html +++ b/app/kubernetes/views/configure/configure.html @@ -42,7 +42,8 @@ -
-
- - + +
+
+
+ + +
+
+
+
+ + +
diff --git a/app/kubernetes/views/configure/configureController.js b/app/kubernetes/views/configure/configureController.js index eb655517b..080c9a6e0 100644 --- a/app/kubernetes/views/configure/configureController.js +++ b/app/kubernetes/views/configure/configureController.js @@ -47,9 +47,10 @@ class KubernetesConfigureController { this.limitedFeature = FeatureId.K8S_SETUP_DEFAULT; this.limitedFeatureAutoWindow = FeatureId.HIDE_AUTO_UPDATE_WINDOW; this.onToggleAutoUpdate = this.onToggleAutoUpdate.bind(this); - this.onChangeAvailability = this.onChangeAvailability.bind(this); + this.onChangeControllers = this.onChangeControllers.bind(this); this.onChangeEnableResourceOverCommit = this.onChangeEnableResourceOverCommit.bind(this); this.onToggleIngressAvailabilityPerNamespace = this.onToggleIngressAvailabilityPerNamespace.bind(this); + this.onToggleAllowNoneIngressClass = this.onToggleAllowNoneIngressClass.bind(this); this.onChangeStorageClassAccessMode = this.onChangeStorageClassAccessMode.bind(this); } /* #endregion */ @@ -71,7 +72,7 @@ class KubernetesConfigureController { /* #endregion */ /* #region INGRESS CLASSES UI MANAGEMENT */ - onChangeAvailability(controllerClassMap) { + onChangeControllers(controllerClassMap) { this.ingressControllers = controllerClassMap; } @@ -79,6 +80,18 @@ class KubernetesConfigureController { return _.find(this.formValues.IngressClasses, { Type: this.IngressClassTypes.TRAEFIK }); } + toggleAdvancedIngSettings() { + this.$scope.$evalAsync(() => { + this.state.isIngToggleSectionExpanded = !this.state.isIngToggleSectionExpanded; + }); + } + + onToggleAllowNoneIngressClass() { + this.$scope.$evalAsync(() => { + this.formValues.AllowNoneIngressClass = !this.formValues.AllowNoneIngressClass; + }); + } + onToggleIngressAvailabilityPerNamespace() { this.$scope.$evalAsync(() => { this.formValues.IngressAvailabilityPerNamespace = !this.formValues.IngressAvailabilityPerNamespace; @@ -109,6 +122,7 @@ class KubernetesConfigureController { endpoint.Kubernetes.Configuration.IngressClasses = ingressClasses; endpoint.Kubernetes.Configuration.RestrictDefaultNamespace = this.formValues.RestrictDefaultNamespace; endpoint.Kubernetes.Configuration.IngressAvailabilityPerNamespace = this.formValues.IngressAvailabilityPerNamespace; + endpoint.Kubernetes.Configuration.AllowNoneIngressClass = this.formValues.AllowNoneIngressClass; endpoint.ChangeWindow = this.state.autoUpdateSettings; } @@ -256,6 +270,7 @@ class KubernetesConfigureController { actionInProgress: false, displayConfigureClassPanel: {}, viewReady: false, + isIngToggleSectionExpanded: false, endpointId: this.$state.params.endpointId, duplicates: { ingressClasses: new KubernetesFormValidationReferences(), @@ -315,6 +330,7 @@ class KubernetesConfigureController { return ic; }); this.formValues.IngressAvailabilityPerNamespace = this.endpoint.Kubernetes.Configuration.IngressAvailabilityPerNamespace; + this.formValues.AllowNoneIngressClass = this.endpoint.Kubernetes.Configuration.AllowNoneIngressClass; this.oldFormValues = Object.assign({}, this.formValues); } catch (err) { diff --git a/app/kubernetes/views/resource-pools/create/createResourcePool.html b/app/kubernetes/views/resource-pools/create/createResourcePool.html index 56a88b40a..dbe1365e9 100644 --- a/app/kubernetes/views/resource-pools/create/createResourcePool.html +++ b/app/kubernetes/views/resource-pools/create/createResourcePool.html @@ -185,7 +185,7 @@
Networking
Networking
setIsExpanded(!isExpanded)} - className="border-0 mx-2 bg-transparent inline-flex justify-center items-center w-2" + className="border-0 mx-2 !ml-0 bg-transparent inline-flex justify-center items-center w-2" > -