diff --git a/pkg/i2gw/providers/gce/gce.go b/pkg/i2gw/providers/gce/gce.go index ef01c3f1..e6eaf34e 100644 --- a/pkg/i2gw/providers/gce/gce.go +++ b/pkg/i2gw/providers/gce/gce.go @@ -24,7 +24,6 @@ import ( "k8s.io/apimachinery/pkg/util/validation/field" ) -// The ProviderName returned to the provider's registry. const ProviderName = "gce" func init() { @@ -57,8 +56,8 @@ func (p *Provider) ReadResourcesFromCluster(ctx context.Context) error { return nil } -func (p *Provider) ReadResourcesFromFile(ctx context.Context, filename string) error { - storage, err := p.reader.readResourcesFromFile(ctx, filename) +func (p *Provider) ReadResourcesFromFile(_ context.Context, filename string) error { + storage, err := p.reader.readResourcesFromFile(filename) if err != nil { return fmt.Errorf("failed to read resources from file: %w", err) } diff --git a/pkg/i2gw/providers/gce/resource_reader.go b/pkg/i2gw/providers/gce/resource_reader.go index 7d860519..ab69a4ba 100644 --- a/pkg/i2gw/providers/gce/resource_reader.go +++ b/pkg/i2gw/providers/gce/resource_reader.go @@ -17,9 +17,17 @@ limitations under the License. package gce import ( + "bytes" "context" + "fmt" + "os" "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw" + "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/providers/common" + networkingv1 "k8s.io/api/networking/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" ) // converter implements the i2gw.CustomResourceReader interface. @@ -35,11 +43,78 @@ func newResourceReader(conf *i2gw.ProviderConf) reader { } func (r *reader) readResourcesFromCluster(ctx context.Context) (*storage, error) { - // read example-gateway related resources from the cluster. - return nil, nil + storage := newResourcesStorage() + + ingresses, err := r.readGCEIngressesFromCluster(ctx, r.conf.Client) + if err != nil { + return nil, err + } + storage.Ingresses = ingresses + + return storage, nil +} + +func (r *reader) readResourcesFromFile(filename string) (*storage, error) { + storage := newResourcesStorage() + + ingresses, err := r.readGCEIngressesFromFile(filename, r.conf.Namespace) + if err != nil { + return nil, err + } + storage.Ingresses = ingresses + + return storage, nil +} + +// readGCEIngressesFromCluster lists ingresses from cluster, validates ingress +// class annotation used by GCE, and return the list of supported ingresses. +func (r *reader) readGCEIngressesFromCluster(ctx context.Context, client client.Client) (map[types.NamespacedName]*networkingv1.Ingress, error) { + var ingressList networkingv1.IngressList + err := client.List(ctx, &ingressList) + if err != nil { + return nil, fmt.Errorf("failed to get ingresses from the cluster: %w", err) + } + + ingresses := map[types.NamespacedName]*networkingv1.Ingress{} + for i, ingress := range ingressList.Items { + if !isSupportedGCEIngressClass(ingress) { + continue + } + ingresses[types.NamespacedName{Namespace: ingress.Namespace, Name: ingress.Name}] = &ingressList.Items[i] + } + + return ingresses, nil } -func (r *reader) readResourcesFromFile(ctx context.Context, filename string) (*storage, error) { - // read example-gateway related resources from the file. - return nil, nil +// readGCEIngressesFromCluster reads ingress configuration from the given file, +// validates ingress class annotation used by GCE, and return the list of +// supported ingresses. +func (r *reader) readGCEIngressesFromFile(filename string, namespace string) (map[types.NamespacedName]*networkingv1.Ingress, error) { + stream, err := os.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("failed to read file %v: %w", filename, err) + } + + unstructuredObjects, err := common.ExtractObjectsFromReader(bytes.NewReader(stream), namespace) + if err != nil { + return nil, fmt.Errorf("failed to extract objects: %w", err) + } + + ingresses := map[types.NamespacedName]*networkingv1.Ingress{} + for _, f := range unstructuredObjects { + if !f.GroupVersionKind().Empty() && f.GroupVersionKind().Kind == "Ingress" { + var ingress networkingv1.Ingress + err = runtime.DefaultUnstructuredConverter. + FromUnstructured(f.UnstructuredContent(), &ingress) + if err != nil { + return nil, err + } + if !isSupportedGCEIngressClass(ingress) { + continue + } + ingresses[types.NamespacedName{Namespace: ingress.Namespace, Name: ingress.Name}] = &ingress + } + + } + return ingresses, nil } diff --git a/pkg/i2gw/providers/gce/types.go b/pkg/i2gw/providers/gce/types.go new file mode 100644 index 00000000..d41ca3de --- /dev/null +++ b/pkg/i2gw/providers/gce/types.go @@ -0,0 +1,25 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package gce + +const ( + gceIngressClass = "gce" + gceL7ILBIngressClass = "gce-internal" + + gceL7GlobalExternalManagedGatewayClass = "gke-l7-global-external-managed" + gceL7RegionalInternalGatewayClass = "gke-l7-rilb" +) diff --git a/pkg/i2gw/providers/gce/utils.go b/pkg/i2gw/providers/gce/utils.go new file mode 100644 index 00000000..a43edbec --- /dev/null +++ b/pkg/i2gw/providers/gce/utils.go @@ -0,0 +1,121 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package gce + +import ( + "fmt" + "strings" + + "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw" + "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/providers/common" + networkingv1 "k8s.io/api/networking/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/validation/field" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +// isSupportedGCEIngressClass verifies if the given ingress is supported by +// GCE. +// `kubernetes.io/ingress.class` annotation is still used by GCE to specify the +// type of ingresses. +// If the value of this annotation is one of the supported values(`gce` or +// `gce-internal`), `ingressClassName` is ignored by GKE Ingress controller. +// If the annotation is not set, the GKE Ingress controller will only process +// the ingress if its ingressClassName is one of the supported values(`gce` or +// `gce-internal`) or empty. +func isSupportedGCEIngressClass(ingress networkingv1.Ingress) bool { + legacyIngressClass := common.GetIngressClass(ingress) + if legacyIngressClass != "" { + return legacyIngressClass == gceIngressClass || legacyIngressClass == gceL7ILBIngressClass + } + if ingress.Spec.IngressClassName == nil { + return true + } + ingressClass := *ingress.Spec.IngressClassName + return ingressClass == gceIngressClass || ingressClass == gceL7ILBIngressClass +} + +// toGceGatewayClass updates the Gateway to use GCE GatewayClass. +func toGceGatewayClass(ingresses []networkingv1.Ingress, gatewayResources *i2gw.GatewayResources) field.ErrorList { + var errs field.ErrorList + + // Since we already validated ingress resources when reading, there are + // only two cases here: + // 1. `kubernetes.io/ingress.class` exists in ingress anntation. In this + // case, we use it to map to the corresponding gateway class. + // 2. Annotation does not exist. In this case, the ingress is defaulted + // to use the GCE external ingress implementation, and should be + // mapped to `gke-l7-gxlb`. + for _, ingress := range ingresses { + gwKey := types.NamespacedName{Namespace: ingress.Namespace, Name: common.GetIngressClass(ingress)} + existingGateway := gatewayResources.Gateways[gwKey] + + newGateway, err := setGCEGatewayClass(ingress, existingGateway) + if err != nil { + errs = append(errs, err) + } + gatewayResources.Gateways[gwKey] = newGateway + } + if len(errs) > 0 { + return errs + } + return nil +} + +// setGCEGatewayClass sets the Gateway to the corresponding GCE GatewayClass. +func setGCEGatewayClass(ingress networkingv1.Ingress, gateway gatewayv1.Gateway) (gatewayv1.Gateway, *field.Error) { + ingressClass := common.GetIngressClass(ingress) + + ingressName := fmt.Sprintf("%s/%s", ingress.Namespace, ingress.Name) + // Get GCE GatewayClass from from GCE Ingress class. + newGatewayClass, err := ingClassToGwyClassGCE(ingressClass) + if err != nil { + return gateway, field.NotSupported(field.NewPath(ingressName).Child("ObjectMeta", "Annotations", "kubernetes.io/ingress.class"), ingressClass, []string{gceIngressClass, gceL7ILBIngressClass}) + } + gateway.Spec.GatewayClassName = gatewayv1.ObjectName(newGatewayClass) + return gateway, nil +} + +// ingClassToGwyClassGCE returns the corresponding GCE Gateway Class based on the +// given GCE ingress class. +func ingClassToGwyClassGCE(ingressClass string) (string, error) { + switch ingressClass { + case gceIngressClass: + return gceL7GlobalExternalManagedGatewayClass, nil + case gceL7ILBIngressClass: + return gceL7RegionalInternalGatewayClass, nil + case "": + return gceL7GlobalExternalManagedGatewayClass, nil + default: + return "", fmt.Errorf("Given GCE Ingress Class not supported") + } +} + +// checkImplementationSpecificPath checks if this ingress contains a +// ImplementationSpecific path with * and throws an error if it does. +func checkImplementationSpecificPath(ingress *networkingv1.Ingress) *field.Error { + for _, rule := range ingress.Spec.Rules { + for _, path := range rule.HTTP.Paths { + if path.PathType != nil && *path.PathType == networkingv1.PathTypeImplementationSpecific && strings.HasSuffix(path.Path, "/*") { + ingressName := fmt.Sprintf("%s/%s", ingress.Namespace, ingress.Name) + detail := "ImplementationSpecific Path with * is not supported for GCE ingress2gateway conversion" + return field.Invalid(field.NewPath(ingressName).Child("Spec", "Rules", "HTTP", "Paths", "Path"), path.Path, detail) + } + } + } + return nil +} diff --git a/pkg/i2gw/providers/gce/utils_test.go b/pkg/i2gw/providers/gce/utils_test.go new file mode 100644 index 00000000..623c457e --- /dev/null +++ b/pkg/i2gw/providers/gce/utils_test.go @@ -0,0 +1,157 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package gce + +import ( + "testing" + + networkingv1 "k8s.io/api/networking/v1" + networkingv1beta1 "k8s.io/api/networking/v1beta1" + meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +func TestIsSupportedGCEIngressClass(t *testing.T) { + t.Parallel() + + ingressClassAnnotationNotSupported := "not-supported" + + ingressClassNameNotSupported := "not-supported" + ingressClassNameGCE := gceIngressClass + ingressClassNameGCEL7ILB := gceL7ILBIngressClass + + testCases := []struct { + desc string + ingress networkingv1.Ingress + expected bool + }{ + { + desc: "annotation not set, ingressClassName not set, supported", + ingress: NewIngress( + types.NamespacedName{}, + map[string]string{}, + networkingv1.IngressSpec{}, + ), + expected: true, + }, + { + desc: "annotation not set, ingressClassName is set to external ingress, supported", + ingress: NewIngress( + types.NamespacedName{}, + map[string]string{}, + networkingv1.IngressSpec{IngressClassName: &ingressClassNameGCE}, + ), + expected: true, + }, + { + desc: "annotation not set, ingressClassName is set to ingress ingress, supported", + ingress: NewIngress( + types.NamespacedName{}, + map[string]string{}, + networkingv1.IngressSpec{IngressClassName: &ingressClassNameGCEL7ILB}, + ), + expected: true, + }, + { + desc: "annotation not set, ingressClassName is set to not supported ingress, not supported", + ingress: NewIngress( + types.NamespacedName{}, + map[string]string{}, + networkingv1.IngressSpec{IngressClassName: &ingressClassNameNotSupported}, + ), + expected: false, + }, + { + desc: "external ingress annotation, ingressClassName not set, supported", + ingress: NewIngress( + types.NamespacedName{}, + map[string]string{networkingv1beta1.AnnotationIngressClass: gceIngressClass}, + networkingv1.IngressSpec{}, + ), + expected: true, + }, + { + desc: "external ingress annotation, ingressClassName is set, supported", + ingress: NewIngress( + types.NamespacedName{}, + map[string]string{networkingv1beta1.AnnotationIngressClass: gceIngressClass}, + networkingv1.IngressSpec{IngressClassName: &ingressClassNameGCE}, + ), + expected: true, + }, + { + desc: "internal ingress annotation, ingressClassName not set, supported", + ingress: NewIngress( + types.NamespacedName{}, + map[string]string{networkingv1beta1.AnnotationIngressClass: gceL7ILBIngressClass}, + networkingv1.IngressSpec{}, + ), + expected: true, + }, + { + desc: "internal ingress annotation, ingressClassName is set, supported", + ingress: NewIngress( + types.NamespacedName{}, + map[string]string{networkingv1beta1.AnnotationIngressClass: gceL7ILBIngressClass}, + networkingv1.IngressSpec{IngressClassName: &ingressClassNameGCEL7ILB}, + ), + expected: true, + }, + { + desc: "not supported ingress annotation, ingressClassName not set, not supported", + ingress: NewIngress( + types.NamespacedName{}, + map[string]string{networkingv1beta1.AnnotationIngressClass: ingressClassAnnotationNotSupported}, + networkingv1.IngressSpec{}, + ), + expected: false, + }, + { + desc: "not supported ingress annotation, ingressClassName is set, not supported", + ingress: NewIngress( + types.NamespacedName{}, + map[string]string{networkingv1beta1.AnnotationIngressClass: ingressClassAnnotationNotSupported}, + networkingv1.IngressSpec{IngressClassName: &ingressClassNameNotSupported}, + ), + expected: false, + }, + } + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + got := isSupportedGCEIngressClass(tc.ingress) + if got != tc.expected { + t.Errorf("isSupportedGCEIngressClass() = %v, expected %v", got, tc.expected) + } + }) + } +} + +// NewIngress returns an Ingress with the given spec. +func NewIngress(name types.NamespacedName, annotations map[string]string, spec networkingv1.IngressSpec) networkingv1.Ingress { + return networkingv1.Ingress{ + TypeMeta: meta_v1.TypeMeta{ + Kind: "Ingress", + APIVersion: "networking/v1", + }, + ObjectMeta: meta_v1.ObjectMeta{ + Name: name.Name, + Namespace: name.Namespace, + Annotations: annotations, + }, + Spec: spec, + } +}