diff --git a/pkg/i2gw/providers/ingressnginx/converter.go b/pkg/i2gw/providers/ingressnginx/converter.go index 8bb16784..cd8055c6 100644 --- a/pkg/i2gw/providers/ingressnginx/converter.go +++ b/pkg/i2gw/providers/ingressnginx/converter.go @@ -19,40 +19,42 @@ package ingressnginx import ( "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/util/validation/field" ) // converter implements the ToGatewayAPI function of i2gw.ResourceConverter interface. type converter struct { - conf *i2gw.ProviderConf - featureParsers []i2gw.FeatureParser } // newConverter returns an ingress-nginx converter instance. -func newConverter(conf *i2gw.ProviderConf) *converter { +func newConverter() *converter { return &converter{ - conf: conf, featureParsers: []i2gw.FeatureParser{ canaryFeature, }, } } -// ToGatewayAPI converts the received i2gw.InputResources to i2gw.GatewayResources -// including the ingress-nginx specific features. -func (c *converter) ToGatewayAPI(resources i2gw.InputResources) (i2gw.GatewayResources, field.ErrorList) { +func (c *converter) convert(storage *storage) (i2gw.GatewayResources, field.ErrorList) { + + // TODO(liorliberman) temporary until we decide to change ToGateway and featureParsers to get a map of [types.NamespacedName]*networkingv1.Ingress instead of a list + ingressList := []networkingv1.Ingress{} + for _, ing := range storage.Ingresses { + ingressList = append(ingressList, *ing) + } // Convert plain ingress resources to gateway resources, ignoring all // provider-specific features. - gatewayResources, errs := common.ToGateway(resources.Ingresses, i2gw.ProviderImplementationSpecificOptions{}) + gatewayResources, errs := common.ToGateway(ingressList, i2gw.ProviderImplementationSpecificOptions{}) if len(errs) > 0 { return i2gw.GatewayResources{}, errs } for _, parseFeatureFunc := range c.featureParsers { // Apply the feature parsing function to the gateway resources, one by one. - parseErrs := parseFeatureFunc(resources, &gatewayResources) + parseErrs := parseFeatureFunc(i2gw.InputResources{Ingresses: ingressList}, &gatewayResources) // Append the parsing errors to the error list. errs = append(errs, parseErrs...) } diff --git a/pkg/i2gw/providers/ingressnginx/converter_test.go b/pkg/i2gw/providers/ingressnginx/converter_test.go index 473f0ad9..94f31014 100644 --- a/pkg/i2gw/providers/ingressnginx/converter_test.go +++ b/pkg/i2gw/providers/ingressnginx/converter_test.go @@ -42,14 +42,14 @@ func Test_ToGateway(t *testing.T) { testCases := []struct { name string - ingresses []networkingv1.Ingress + ingresses map[types.NamespacedName]*networkingv1.Ingress expectedGatewayResources i2gw.GatewayResources expectedErrors field.ErrorList }{ { name: "canary deployment", - ingresses: []networkingv1.Ingress{ - { + ingresses: map[types.NamespacedName]*networkingv1.Ingress{ + {Namespace: "default", Name: "production"}: { ObjectMeta: metav1.ObjectMeta{Name: "production", Namespace: "default"}, Spec: networkingv1.IngressSpec{ IngressClassName: ptrTo("ingress-nginx"), @@ -73,7 +73,7 @@ func Test_ToGateway(t *testing.T) { }}, }, }, - { + {Namespace: "default", Name: "canary"}: { ObjectMeta: metav1.ObjectMeta{ Name: "canary", Namespace: "default", @@ -168,8 +168,8 @@ func Test_ToGateway(t *testing.T) { }, { name: "ImplementationSpecific HTTPRouteMatching", - ingresses: []networkingv1.Ingress{ - { + ingresses: map[types.NamespacedName]*networkingv1.Ingress{ + {Namespace: "default", Name: "implementation-specific-regex"}: { ObjectMeta: metav1.ObjectMeta{ Name: "implementation-specific-regex", Namespace: "default", @@ -215,11 +215,21 @@ func Test_ToGateway(t *testing.T) { provider := NewProvider(&i2gw.ProviderConf{}) - resources := i2gw.InputResources{ - Ingresses: tc.ingresses, - } + nginxProvider := provider.(*Provider) + nginxProvider.storage.Ingresses = tc.ingresses + + // TODO(liorlieberman) we pass an empty i2gw.InputResources temporarily until we decide to change ToGatewayAPI interface + gatewayResources, errs := provider.ToGatewayAPI(i2gw.InputResources{}) - gatewayResources, errs := provider.ToGatewayAPI(resources) + if len(errs) != len(tc.expectedErrors) { + t.Errorf("Expected %d errors, got %d: %+v", len(tc.expectedErrors), len(errs), errs) + } else { + for i, e := range errs { + if errors.Is(e, tc.expectedErrors[i]) { + t.Errorf("Unexpected error message at %d index. Got %s, want: %s", i, e, tc.expectedErrors[i]) + } + } + } if len(gatewayResources.HTTPRoutes) != len(tc.expectedGatewayResources.HTTPRoutes) { t.Errorf("Expected %d HTTPRoutes, got %d: %+v", @@ -248,16 +258,6 @@ func Test_ToGateway(t *testing.T) { } } } - - if len(errs) != len(tc.expectedErrors) { - t.Errorf("Expected %d errors, got %d: %+v", len(tc.expectedErrors), len(errs), errs) - } else { - for i, e := range errs { - if errors.Is(e, tc.expectedErrors[i]) { - t.Errorf("Unexpected error message at %d index. Got %s, want: %s", i, e, tc.expectedErrors[i]) - } - } - } }) } } diff --git a/pkg/i2gw/providers/ingressnginx/ingressnginx.go b/pkg/i2gw/providers/ingressnginx/ingressnginx.go index 56acaf19..21c83970 100644 --- a/pkg/i2gw/providers/ingressnginx/ingressnginx.go +++ b/pkg/i2gw/providers/ingressnginx/ingressnginx.go @@ -17,11 +17,16 @@ limitations under the License. package ingressnginx import ( + "context" + "fmt" + "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw" + "k8s.io/apimachinery/pkg/util/validation/field" ) // The Name of the provider. const Name = "ingress-nginx" +const NginxIngressClass = "nginx" func init() { i2gw.ProviderConstructorByName[Name] = NewProvider @@ -29,17 +34,42 @@ func init() { // Provider implements the i2gw.Provider interface. type Provider struct { - conf *i2gw.ProviderConf - - *resourceReader - *converter + storage *storage + resourceReader *resourceReader + converter *converter } // NewProvider constructs and returns the ingress-nginx implementation of i2gw.Provider. func NewProvider(conf *i2gw.ProviderConf) i2gw.Provider { return &Provider{ - conf: conf, + storage: newResourcesStorage(), resourceReader: newResourceReader(conf), - converter: newConverter(conf), + converter: newConverter(), + } +} + +// ToGatewayAPI converts the received i2gw.InputResources to i2gw.GatewayResources +// including the ingress-nginx specific features. +func (p *Provider) ToGatewayAPI(_ i2gw.InputResources) (i2gw.GatewayResources, field.ErrorList) { + return p.converter.convert(p.storage) +} + +func (p *Provider) ReadResourcesFromCluster(ctx context.Context) error { + storage, err := p.resourceReader.readResourcesFromCluster(ctx) + if err != nil { + return fmt.Errorf("failed to read resources from cluster: %w", err) + } + + p.storage = storage + return nil +} + +func (p *Provider) ReadResourcesFromFile(ctx context.Context, filename string) error { + storage, err := p.resourceReader.readResourcesFromFile(ctx, filename) + if err != nil { + return fmt.Errorf("failed to read resources from file: %w", err) } + + p.storage = storage + return nil } diff --git a/pkg/i2gw/providers/ingressnginx/resource_reader.go b/pkg/i2gw/providers/ingressnginx/resource_reader.go index 307ae724..a61bbc19 100644 --- a/pkg/i2gw/providers/ingressnginx/resource_reader.go +++ b/pkg/i2gw/providers/ingressnginx/resource_reader.go @@ -17,9 +17,16 @@ limitations under the License. package ingressnginx 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" ) // converter implements the i2gw.CustomResourceReader interface. @@ -34,12 +41,51 @@ func newResourceReader(conf *i2gw.ProviderConf) *resourceReader { } } -func (r *resourceReader) ReadResourcesFromCluster(_ context.Context) error { - // ingress-nginx does not have any CRDs. - return nil +func (r *resourceReader) readResourcesFromCluster(ctx context.Context) (*storage, error) { + storage := newResourcesStorage() + + var ingressList networkingv1.IngressList + err := r.conf.Client.List(ctx, &ingressList) + if err != nil { + return nil, fmt.Errorf("failed to get ingresses from the cluster: %w", err) + } + + for i, ingress := range ingressList.Items { + if common.GetIngressClass(ingress) != NginxIngressClass { + continue + } + storage.Ingresses[types.NamespacedName{Namespace: ingress.Namespace, Name: ingress.Name}] = &ingressList.Items[i] + } + + return storage, nil } -func (r *resourceReader) ReadResourcesFromFile(_ context.Context, _ string) error { - // ingress-nginx does not have any CRDs. - return nil +func (r *resourceReader) readResourcesFromFile(_ context.Context, filename string) (*storage, error) { + storage := newResourcesStorage() + stream, err := os.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("failed to read file %v: %w", filename, err) + } + + unstructuredObjects, err := i2gw.ExtractObjectsFromReader(bytes.NewReader(stream), r.conf.Namespace) + if err != nil { + return nil, fmt.Errorf("failed to extract objects: %w", err) + } + + for _, f := range unstructuredObjects { + if !f.GroupVersionKind().Empty() && f.GroupVersionKind().Kind == "Ingress" { + var i networkingv1.Ingress + err = runtime.DefaultUnstructuredConverter. + FromUnstructured(f.UnstructuredContent(), &i) + if err != nil { + return nil, err + } + if common.GetIngressClass(i) != NginxIngressClass { + continue + } + storage.Ingresses[types.NamespacedName{Namespace: i.Namespace, Name: i.Name}] = &i + } + + } + return storage, nil } diff --git a/pkg/i2gw/providers/ingressnginx/storage.go b/pkg/i2gw/providers/ingressnginx/storage.go new file mode 100644 index 00000000..6b6a3ca9 --- /dev/null +++ b/pkg/i2gw/providers/ingressnginx/storage.go @@ -0,0 +1,32 @@ +/* +Copyright 2023 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 ingressnginx + +import ( + networkingv1 "k8s.io/api/networking/v1" + "k8s.io/apimachinery/pkg/types" +) + +type storage struct { + Ingresses map[types.NamespacedName]*networkingv1.Ingress +} + +func newResourcesStorage() *storage { + return &storage{ + Ingresses: map[types.NamespacedName]*networkingv1.Ingress{}, + } +}