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' && ( + + )}