diff --git a/.aspell.yml b/.aspell.yml index f91fd095..616998b5 100644 --- a/.aspell.yml +++ b/.aspell.yml @@ -26,3 +26,4 @@ allowed: - crd - linter - linters + - tls diff --git a/go.mod b/go.mod index 022699be..09bccbe3 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/fasthttp/router v1.4.20 github.com/go-openapi/swag v0.23.0 github.com/go-test/deep v1.1.0 + github.com/google/go-cmp v0.6.0 github.com/google/renameio v1.0.1 github.com/haproxytech/client-native/v3 v3.1.2-0.20230607075433-231591da68ed github.com/haproxytech/client-native/v5 v5.1.11 @@ -51,7 +52,6 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/haproxytech/go-logger v1.1.0 // indirect diff --git a/main.go b/main.go index 6a4c6232..bfcabf37 100644 --- a/main.go +++ b/main.go @@ -36,6 +36,7 @@ import ( "github.com/haproxytech/kubernetes-ingress/pkg/controller" "github.com/haproxytech/kubernetes-ingress/pkg/job" "github.com/haproxytech/kubernetes-ingress/pkg/k8s" + "github.com/haproxytech/kubernetes-ingress/pkg/k8s/meta" k8ssync "github.com/haproxytech/kubernetes-ingress/pkg/k8s/sync" "github.com/haproxytech/kubernetes-ingress/pkg/store" "github.com/haproxytech/kubernetes-ingress/pkg/utils" @@ -140,6 +141,10 @@ func main() { publishService, ) + if osArgs.Test { + meta.GetMetaStore().ProcessedResourceVersion.SetTestMode() + } + c := controller.NewBuilder(). WithHaproxyCfgFile(haproxyConf). WithEventChan(eventChan). diff --git a/pkg/controller/monitor.go b/pkg/controller/monitor.go index a1feac18..b9bb6f76 100644 --- a/pkg/controller/monitor.go +++ b/pkg/controller/monitor.go @@ -60,7 +60,7 @@ func (c *HAProxyController) SyncData() { case k8ssync.NAMESPACE: change = c.store.EventNamespace(ns, job.Data.(*store.Namespace)) //nolint:forcetypeassert case k8ssync.INGRESS: - change = c.store.EventIngress(ns, job.Data.(*store.Ingress)) //nolint:forcetypeassert + change = c.store.EventIngress(ns, job.Data.(*store.Ingress), job.UID, job.ResourceVersion) //nolint:forcetypeassert case k8ssync.INGRESS_CLASS: change = c.store.EventIngressClass(job.Data.(*store.IngressClass)) //nolint:forcetypeassert case k8ssync.ENDPOINTS: diff --git a/pkg/handler/prometheus.go b/pkg/handler/prometheus.go index ef183052..0d2eed88 100644 --- a/pkg/handler/prometheus.go +++ b/pkg/handler/prometheus.go @@ -123,7 +123,7 @@ func (handler PrometheusEndpoint) Update(k store.K8s, h haproxy.HAProxy, a annot } if userListChanged || status != store.EMPTY || secretExists && secret.Status != store.EMPTY { - k.EventIngress(k.GetNamespace(ing.Namespace), ing) + k.EventIngress(k.GetNamespace(ing.Namespace), ing, "fakeUID", "fakeResourceVersion") } instance.ReloadIf(status != store.EMPTY, "creation/modification of prometheus endpoint") diff --git a/pkg/k8s/informer-utils.go b/pkg/k8s/informer-utils.go new file mode 100644 index 00000000..05222bc5 --- /dev/null +++ b/pkg/k8s/informer-utils.go @@ -0,0 +1,32 @@ +// Copyright 2019 HAProxy Technologies LLC +// +// 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 k8s + +import ( + k8smeta "github.com/haproxytech/kubernetes-ingress/pkg/k8s/meta" + k8ssync "github.com/haproxytech/kubernetes-ingress/pkg/k8s/sync" + "k8s.io/apimachinery/pkg/types" +) + +func ToSyncDataEvent(meta k8smeta.MetaInfoer, data interface{}, uid types.UID, resourceVersion string) k8ssync.SyncDataEvent { + return k8ssync.SyncDataEvent{ + SyncType: meta.GetType(), + Namespace: meta.GetNamespace(), + Name: meta.GetName(), + Data: data, + UID: uid, + ResourceVersion: resourceVersion, + } +} diff --git a/pkg/k8s/informers.go b/pkg/k8s/informers.go index 445b8538..85a523f3 100644 --- a/pkg/k8s/informers.go +++ b/pkg/k8s/informers.go @@ -16,7 +16,9 @@ import ( "github.com/haproxytech/client-native/v5/models" v1 "github.com/haproxytech/kubernetes-ingress/crs/api/ingress/v1" + k8smeta "github.com/haproxytech/kubernetes-ingress/pkg/k8s/meta" k8ssync "github.com/haproxytech/kubernetes-ingress/pkg/k8s/sync" + k8stransform "github.com/haproxytech/kubernetes-ingress/pkg/k8s/transform" "github.com/haproxytech/kubernetes-ingress/pkg/store" "github.com/haproxytech/kubernetes-ingress/pkg/utils" gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" @@ -63,8 +65,8 @@ func (k k8s) getNamespaceInfomer(eventChan chan k8ssync.SyncDataEvent, factory i Labels: utils.CopyMap(data.Labels), Status: status, } - logger.Tracef("[RUNTIME] [K8s] %s %s: %s", k8ssync.NAMESPACE, item.Status, item.Name) - eventChan <- k8ssync.SyncDataEvent{SyncType: k8ssync.NAMESPACE, Namespace: item.Name, Data: item} + logIncomingK8sEvent(logger, item, data.UID, data.ResourceVersion) + eventChan <- ToSyncDataEvent(item, item, data.UID, data.ResourceVersion) }, DeleteFunc: func(obj interface{}) { data, ok := obj.(*corev1.Namespace) @@ -92,8 +94,8 @@ func (k k8s) getNamespaceInfomer(eventChan chan k8ssync.SyncDataEvent, factory i Labels: utils.CopyMap(data.Labels), Status: status, } - logger.Tracef("[RUNTIME] [K8s] %s %s: %s", k8ssync.NAMESPACE, item.Status, item.Name) - eventChan <- k8ssync.SyncDataEvent{SyncType: k8ssync.NAMESPACE, Namespace: item.Name, Data: item} + logIncomingK8sEvent(logger, item, data.UID, data.ResourceVersion) + eventChan <- ToSyncDataEvent(item, item, data.UID, data.ResourceVersion) }, UpdateFunc: func(oldObj, newObj interface{}) { _, ok := oldObj.(*corev1.Namespace) @@ -113,12 +115,16 @@ func (k k8s) getNamespaceInfomer(eventChan chan k8ssync.SyncDataEvent, factory i Status: status, Labels: utils.CopyMap(data2.Labels), } - logger.Tracef("[RUNTIME] [K8s] %s %s: %s", k8ssync.NAMESPACE, item2.Status, item2.Name) - eventChan <- k8ssync.SyncDataEvent{SyncType: k8ssync.NAMESPACE, Namespace: item2.Name, Data: item2} + logIncomingK8sEvent(logger, item2, data2.UID, data2.ResourceVersion) + eventChan <- ToSyncDataEvent(item2, item2, data2.UID, data2.ResourceVersion) }, }, ) logger.Error(err) + + // Use TransformFunc to modify/filter objects before passing them to handlers + err = informer.SetTransform(k8stransform.TransformNamespace) + logger.Error(err) return informer } @@ -161,13 +167,8 @@ func (k k8s) getServiceInformer(eventChan chan k8ssync.SyncDataEvent, factory in Port: int64(sp.Port), }) } - logger.Tracef("[RUNTIME] [K8s] %s %s: %s", k8ssync.SERVICE, item.Status, item.Name) - eventChan <- k8ssync.SyncDataEvent{ - SyncType: k8ssync.SERVICE, - Namespace: item.Namespace, - Name: item.Name, - Data: item, - } + logIncomingK8sEvent(logger, item, data.UID, data.ResourceVersion) + eventChan <- ToSyncDataEvent(item, item, data.UID, data.ResourceVersion) if k.publishSvc != nil && k.publishSvc.Namespace == item.Namespace && k.publishSvc.Name == item.Name { // item copy because of ADDED handler in events.go which must modify the STATUS based solely on addresses itemCopy := *item @@ -200,13 +201,8 @@ func (k k8s) getServiceInformer(eventChan chan k8ssync.SyncDataEvent, factory in if data.Spec.Type == corev1.ServiceTypeExternalName { item.DNS = data.Spec.ExternalName } - logger.Tracef("[RUNTIME] [K8s] %s %s: %s", k8ssync.SERVICE, item.Status, item.Name) - eventChan <- k8ssync.SyncDataEvent{ - SyncType: k8ssync.SERVICE, - Namespace: item.Namespace, - Name: item.Name, - Data: item, - } + logIncomingK8sEvent(logger, item, data.UID, data.ResourceVersion) + eventChan <- ToSyncDataEvent(item, item, data.UID, data.ResourceVersion) if k.publishSvc != nil && k.publishSvc.Namespace == item.Namespace && k.publishSvc.Name == item.Name { item.Addresses = getServiceAddresses(data) logger.Tracef("[RUNTIME] [K8s] %s %s: %s", k8ssync.PUBLISH_SERVICE, item.Status, item.Name) @@ -258,13 +254,8 @@ func (k k8s) getServiceInformer(eventChan chan k8ssync.SyncDataEvent, factory in }) } - logger.Tracef("[RUNTIME] [K8s] %s %s: %s", k8ssync.SERVICE, item2.Status, item2.Name) - eventChan <- k8ssync.SyncDataEvent{ - SyncType: k8ssync.SERVICE, - Namespace: item2.Namespace, - Name: item2.Name, - Data: item2, - } + logIncomingK8sEvent(logger, item2, data2.UID, data2.ResourceVersion) + eventChan <- ToSyncDataEvent(item2, item2, data2.UID, data2.ResourceVersion) if k.publishSvc != nil && k.publishSvc.Namespace == item2.Namespace && k.publishSvc.Name == item2.Name { item2.Addresses = getServiceAddresses(data2) @@ -279,6 +270,10 @@ func (k k8s) getServiceInformer(eventChan chan k8ssync.SyncDataEvent, factory in }, }) logger.Error(err) + + // Use TransformFunc to modify/filter objects before passing them to handlers + err = informer.SetTransform(k8stransform.TransformService) + logger.Error(err) return informer } @@ -307,13 +302,8 @@ func (k k8s) getSecretInformer(eventChan chan k8ssync.SyncDataEvent, factory inf Data: data.Data, Status: status, } - logger.Tracef("[RUNTIME] [K8s] %s %s: %s", k8ssync.SECRET, item.Status, item.Name) - eventChan <- k8ssync.SyncDataEvent{ - SyncType: k8ssync.SECRET, - Namespace: item.Namespace, - Name: item.Name, - Data: item, - } + logIncomingK8sEvent(logger, item, data.UID, data.ResourceVersion) + eventChan <- ToSyncDataEvent(item, item, data.UID, data.ResourceVersion) }, DeleteFunc: func(obj interface{}) { data, ok := obj.(*corev1.Secret) @@ -328,13 +318,8 @@ func (k k8s) getSecretInformer(eventChan chan k8ssync.SyncDataEvent, factory inf Data: data.Data, Status: status, } - logger.Tracef("[RUNTIME] [K8s] %s %s: %s", k8ssync.SECRET, item.Status, item.Name) - eventChan <- k8ssync.SyncDataEvent{ - SyncType: k8ssync.SECRET, - Namespace: item.Namespace, - Name: item.Name, - Data: item, - } + logIncomingK8sEvent(logger, item, data.UID, data.ResourceVersion) + eventChan <- ToSyncDataEvent(item, item, data.UID, data.ResourceVersion) }, UpdateFunc: func(oldObj, newObj interface{}) { _, ok := oldObj.(*corev1.Secret) @@ -356,17 +341,16 @@ func (k k8s) getSecretInformer(eventChan chan k8ssync.SyncDataEvent, factory inf Status: status, } - logger.Tracef("[RUNTIME] [K8s] %s %s: %s", k8ssync.SECRET, item2.Status, item2.Name) - eventChan <- k8ssync.SyncDataEvent{ - SyncType: k8ssync.SECRET, - Namespace: item2.Namespace, - Name: item2.Name, - Data: item2, - } + logIncomingK8sEvent(logger, item2, data2.UID, data2.ResourceVersion) + eventChan <- ToSyncDataEvent(item2, item2, data2.UID, data2.ResourceVersion) }, }, ) logger.Error(err) + + // Use TransformFunc to modify/filter objects before passing them to handlers + err = informer.SetTransform(k8stransform.TransformSecret) + logger.Error(err) return informer } @@ -395,13 +379,8 @@ func (k k8s) getConfigMapInformer(eventChan chan k8ssync.SyncDataEvent, factory Annotations: store.CopyAnnotations(data.Data), Status: status, } - logger.Tracef("[RUNTIME] [K8s] %s %s: %s", k8ssync.CONFIGMAP, item.Status, item.Name) - eventChan <- k8ssync.SyncDataEvent{ - SyncType: k8ssync.CONFIGMAP, - Namespace: item.Namespace, - Name: item.Name, - Data: item, - } + logIncomingK8sEvent(logger, item, data.UID, data.ResourceVersion) + eventChan <- ToSyncDataEvent(item, item, data.UID, data.ResourceVersion) }, DeleteFunc: func(obj interface{}) { data, ok := obj.(*corev1.ConfigMap) @@ -416,13 +395,8 @@ func (k k8s) getConfigMapInformer(eventChan chan k8ssync.SyncDataEvent, factory Annotations: store.CopyAnnotations(data.Data), Status: status, } - logger.Tracef("[RUNTIME] [K8s] %s %s: %s", k8ssync.CONFIGMAP, item.Status, item.Name) - eventChan <- k8ssync.SyncDataEvent{ - SyncType: k8ssync.CONFIGMAP, - Namespace: item.Namespace, - Name: item.Name, - Data: item, - } + logIncomingK8sEvent(logger, item, data.UID, data.ResourceVersion) + eventChan <- ToSyncDataEvent(item, item, data.UID, data.ResourceVersion) }, UpdateFunc: func(oldObj, newObj interface{}) { _, ok := oldObj.(*corev1.ConfigMap) @@ -443,17 +417,16 @@ func (k k8s) getConfigMapInformer(eventChan chan k8ssync.SyncDataEvent, factory Status: status, } - logger.Tracef("[RUNTIME] [K8s] %s %s: %s", k8ssync.CONFIGMAP, item2.Status, item2.Name) - eventChan <- k8ssync.SyncDataEvent{ - SyncType: k8ssync.CONFIGMAP, - Namespace: item2.Namespace, - Name: item2.Name, - Data: item2, - } + logIncomingK8sEvent(logger, item2, data2.UID, data2.ResourceVersion) + eventChan <- ToSyncDataEvent(item2, item2, data2.UID, data2.ResourceVersion) }, }, ) logger.Error(err) + + // Use TransformFunc to modify/filter objects before passing them to handlers + err = informer.SetTransform(k8stransform.TransformSecret) + logger.Error(err) return informer } @@ -521,26 +494,20 @@ func (k k8s) getEndpointsInformer(eventChan chan k8ssync.SyncDataEvent, factory if errors.Is(err, ErrIgnored) { return } - logger.Tracef("[RUNTIME] [K8s] %s %s: %s %s", k8ssync.ENDPOINTS, item.Status, item.Service, item.SliceName) - eventChan <- k8ssync.SyncDataEvent{ - SyncType: k8ssync.ENDPOINTS, - Namespace: item.Namespace, - Name: item.SliceName, - Data: item, - } + uid, resourceVersion, err := store.GetUIDResourceVersion(obj) + logger.Error(err) + logIncomingK8sEvent(logger, item, uid, resourceVersion) + eventChan <- ToSyncDataEvent(item, item, uid, resourceVersion) }, DeleteFunc: func(obj interface{}) { item, err := k.convertToEndpoints(obj, store.DELETED) if errors.Is(err, ErrIgnored) { return } - logger.Tracef("[RUNTIME] [K8s] %s %s: %s %s", k8ssync.ENDPOINTS, item.Status, item.Service, item.SliceName) - eventChan <- k8ssync.SyncDataEvent{ - SyncType: k8ssync.ENDPOINTS, - Namespace: item.Namespace, - Name: item.SliceName, - Data: item, - } + uid, resourceVersion, err := store.GetUIDResourceVersion(obj) + logger.Error(err) + logIncomingK8sEvent(logger, item, uid, resourceVersion) + eventChan <- ToSyncDataEvent(item, item, uid, resourceVersion) }, UpdateFunc: func(oldObj, newObj interface{}) { _, err := k.convertToEndpoints(oldObj, store.EMPTY) @@ -549,16 +516,17 @@ func (k k8s) getEndpointsInformer(eventChan chan k8ssync.SyncDataEvent, factory } item2, _ := k.convertToEndpoints(newObj, store.MODIFIED) // fix modified state for ones that are deleted,new,same - logger.Tracef("[RUNTIME] [K8s] %s %s: %s %s", k8ssync.ENDPOINTS, item2.Status, item2.Service, item2.SliceName) - eventChan <- k8ssync.SyncDataEvent{ - SyncType: k8ssync.ENDPOINTS, - Namespace: item2.Namespace, - Name: item2.SliceName, - Data: item2, - } + uid, resourceVersion, err := store.GetUIDResourceVersion(newObj) + logger.Error(err) + logIncomingK8sEvent(logger, item2, uid, resourceVersion) + eventChan <- ToSyncDataEvent(item2, item2, uid, resourceVersion) }, }) logger.Error(err) + + // Use TransformFunc to modify/filter objects before passing them to handlers + err = informer.SetTransform(k8stransform.TransformEndpoints) + logger.Error(err) return informer } @@ -577,12 +545,8 @@ func (k *k8s) getPodInformer(namespace, podPrefix string, resyncPeriod time.Dura if prefix != podPrefix { return } - eventChan <- k8ssync.SyncDataEvent{ - SyncType: k8ssync.POD, - Namespace: meta.Namespace, - Name: meta.Name, - Data: store.PodEvent{Status: store.ADDED, Name: meta.Name}, - } + item := store.PodEvent{Status: store.ADDED, Name: meta.Name, Namespace: meta.Namespace} + eventChan <- ToSyncDataEvent(item, item, meta.UID, meta.ResourceVersion) }, DeleteFunc: func(obj interface{}) { meta := obj.(*corev1.Pod).ObjectMeta //nolint:forcetypeassert @@ -590,12 +554,8 @@ func (k *k8s) getPodInformer(namespace, podPrefix string, resyncPeriod time.Dura if prefix != podPrefix { return } - eventChan <- k8ssync.SyncDataEvent{ - SyncType: k8ssync.POD, - Namespace: meta.Namespace, - Name: meta.Name, - Data: store.PodEvent{Status: store.DELETED, Name: meta.Name}, - } + item := store.PodEvent{Status: store.DELETED, Name: meta.Name, Namespace: meta.Namespace} + eventChan <- ToSyncDataEvent(item, item, meta.UID, meta.ResourceVersion) }, UpdateFunc: func(oldObj, newObj interface{}) { meta := newObj.(*corev1.Pod).ObjectMeta //nolint:forcetypeassert @@ -603,15 +563,12 @@ func (k *k8s) getPodInformer(namespace, podPrefix string, resyncPeriod time.Dura if prefix != podPrefix { return } - eventChan <- k8ssync.SyncDataEvent{ - SyncType: k8ssync.POD, - Namespace: meta.Namespace, - Name: meta.Name, - Data: store.PodEvent{Status: store.MODIFIED, Name: meta.Name}, - } + item := store.PodEvent{Status: store.MODIFIED, Name: meta.Name, Namespace: meta.Namespace} + eventChan <- ToSyncDataEvent(item, item, meta.UID, meta.ResourceVersion) }, }, }) + return eController } @@ -628,8 +585,10 @@ func (k k8s) addIngressClassHandlers(eventChan chan k8ssync.SyncDataEvent, infor logger.Errorf("%s: Invalid data from k8s api, %s", k8ssync.INGRESS_CLASS, obj) return } - logger.Tracef("[RUNTIME] [K8s] %s %s: %s", k8ssync.INGRESS_CLASS, item.Status, item.Name) - eventChan <- k8ssync.SyncDataEvent{SyncType: k8ssync.INGRESS_CLASS, Data: item} + uid, resourceVersion, err := store.GetUIDResourceVersion(obj) + logger.Error(err) + logIncomingK8sEvent(logger, item, uid, resourceVersion) + eventChan <- ToSyncDataEvent(item, item, uid, resourceVersion) }, DeleteFunc: func(obj interface{}) { item, err := store.ConvertToIngressClass(obj) @@ -638,8 +597,10 @@ func (k k8s) addIngressClassHandlers(eventChan chan k8ssync.SyncDataEvent, infor return } item.Status = store.DELETED - logger.Tracef("[RUNTIME] [K8s] %s %s: %s", k8ssync.INGRESS_CLASS, item.Status, item.Name) - eventChan <- k8ssync.SyncDataEvent{SyncType: k8ssync.INGRESS_CLASS, Data: item} + uid, resourceVersion, err := store.GetUIDResourceVersion(obj) + logger.Error(err) + logIncomingK8sEvent(logger, item, uid, resourceVersion) + eventChan <- ToSyncDataEvent(item, item, uid, resourceVersion) }, UpdateFunc: func(oldObj, newObj interface{}) { item, err := store.ConvertToIngressClass(newObj) @@ -648,13 +609,18 @@ func (k k8s) addIngressClassHandlers(eventChan chan k8ssync.SyncDataEvent, infor return } item.Status = store.MODIFIED - - logger.Tracef("[RUNTIME] [K8s] %s %s: %s", k8ssync.INGRESS_CLASS, item.Status, item.Name) - eventChan <- k8ssync.SyncDataEvent{SyncType: k8ssync.INGRESS_CLASS, Data: item} + uid, resourceVersion, err := store.GetUIDResourceVersion(newObj) + logger.Error(err) + logIncomingK8sEvent(logger, item, uid, resourceVersion) + eventChan <- ToSyncDataEvent(item, item, uid, resourceVersion) }, }, ) logger.Error(err) + + // Use TransformFunc to modify/filter objects before passing them to handlers + err = informer.SetTransform(k8stransform.TransformIngressClass) + logger.Error(err) } func (k k8s) addIngressHandlers(eventChan chan k8ssync.SyncDataEvent, informer cache.SharedIndexInformer) { @@ -671,13 +637,10 @@ func (k k8s) addIngressHandlers(eventChan chan k8ssync.SyncDataEvent, informer c return } item.Status = store.ADDED - logger.Tracef("[RUNTIME] [K8s] %s %s: %s", k8ssync.INGRESS, item.Status, item.Name) - eventChan <- k8ssync.SyncDataEvent{ - SyncType: k8ssync.INGRESS, - Namespace: item.Namespace, - Name: item.Name, - Data: item, - } + uid, resourceVersion, err := store.GetUIDResourceVersion(obj) + logger.Error(err) + logIncomingK8sEvent(logger, item, uid, resourceVersion) + eventChan <- ToSyncDataEvent(item, item, uid, resourceVersion) }, DeleteFunc: func(obj interface{}) { item, err := store.ConvertToIngress(obj) @@ -686,13 +649,10 @@ func (k k8s) addIngressHandlers(eventChan chan k8ssync.SyncDataEvent, informer c return } item.Status = store.DELETED - logger.Tracef("[RUNTIME] [K8s] %s %s: %s", k8ssync.INGRESS, item.Status, item.Name) - eventChan <- k8ssync.SyncDataEvent{ - SyncType: k8ssync.INGRESS, - Namespace: item.Namespace, - Name: item.Name, - Data: item, - } + uid, resourceVersion, err := store.GetUIDResourceVersion(obj) + logger.Error(err) + logIncomingK8sEvent(logger, item, uid, resourceVersion) + eventChan <- ToSyncDataEvent(item, item, uid, resourceVersion) }, UpdateFunc: func(oldObj, newObj interface{}) { item, err := store.ConvertToIngress(newObj) @@ -701,17 +661,22 @@ func (k k8s) addIngressHandlers(eventChan chan k8ssync.SyncDataEvent, informer c return } item.Status = store.MODIFIED - logger.Tracef("[RUNTIME] [K8s] %s %s: %s", k8ssync.INGRESS, item.Status, item.Name) - eventChan <- k8ssync.SyncDataEvent{ - SyncType: k8ssync.INGRESS, - Namespace: item.Namespace, - Name: item.Name, - Data: item, + uid, resourceVersion, err := store.GetUIDResourceVersion(newObj) + logger.Error(err) + if k8smeta.GetMetaStore().ProcessedResourceVersion.IsProcessed(item, uid, resourceVersion) { + logIncomingK8sEvent(logger, item, uid, resourceVersion, "already processed") + return } + logIncomingK8sEvent(logger, item, uid, resourceVersion, "new resource version") + eventChan <- ToSyncDataEvent(item, item, uid, resourceVersion) }, }, ) logger.Error(err) + + // Use TransformFunc to modify/filter objects before passing them to handlers + err = informer.SetTransform(k8stransform.TransformIngress) + logger.Error(err) } func (k k8s) addEndpointSliceHandlers(eventChan chan k8ssync.SyncDataEvent, informer cache.SharedIndexInformer) { @@ -725,26 +690,20 @@ func (k k8s) addEndpointSliceHandlers(eventChan chan k8ssync.SyncDataEvent, info if errors.Is(err, ErrIgnored) { return } - logger.Tracef("[RUNTIME] [K8s] %s %s: %s %s", k8ssync.ENDPOINTS, item.Status, item.Service, item.SliceName) - eventChan <- k8ssync.SyncDataEvent{ - SyncType: k8ssync.ENDPOINTS, - Namespace: item.Namespace, - Name: item.SliceName, - Data: item, - } + uid, resourceVersion, err := store.GetUIDResourceVersion(obj) + logger.Error(err) + logIncomingK8sEvent(logger, item, uid, resourceVersion) + eventChan <- ToSyncDataEvent(item, item, uid, resourceVersion) }, DeleteFunc: func(obj interface{}) { item, err := k.convertToEndpoints(obj, store.DELETED) if errors.Is(err, ErrIgnored) { return } - logger.Tracef("[RUNTIME] [K8s] %s %s: %s %s", k8ssync.ENDPOINTS, item.Status, item.Service, item.SliceName) - eventChan <- k8ssync.SyncDataEvent{ - SyncType: k8ssync.ENDPOINTS, - Namespace: item.Namespace, - Name: item.SliceName, - Data: item, - } + uid, resourceVersion, err := store.GetUIDResourceVersion(obj) + logger.Error(err) + logIncomingK8sEvent(logger, item, uid, resourceVersion) + eventChan <- ToSyncDataEvent(item, item, uid, resourceVersion) }, UpdateFunc: func(oldObj, newObj interface{}) { _, err := k.convertToEndpoints(oldObj, store.EMPTY) @@ -752,17 +711,18 @@ func (k k8s) addEndpointSliceHandlers(eventChan chan k8ssync.SyncDataEvent, info return } item2, _ := k.convertToEndpoints(newObj, store.MODIFIED) + uid, resourceVersion, err := store.GetUIDResourceVersion(newObj) + logger.Error(err) // fix modified state for ones that are deleted,new,same - logger.Tracef("[RUNTIME] [K8s] %s %s: %s %s", k8ssync.ENDPOINTS, item2.Status, item2.Service, item2.SliceName) - eventChan <- k8ssync.SyncDataEvent{ - SyncType: k8ssync.ENDPOINTS, - Namespace: item2.Namespace, - Name: item2.SliceName, - Data: item2, - } + logIncomingK8sEvent(logger, item2, uid, resourceVersion) + eventChan <- ToSyncDataEvent(item2, item2, uid, resourceVersion) }, }) logger.Error(err) + + // Use TransformFunc to modify/filter objects before passing them to handlers + err = informer.SetTransform(k8stransform.TransformEndpoints) + logger.Error(err) } func (k k8s) convertToEndpoints(obj interface{}, status store.Status) (*store.Endpoints, error) { diff --git a/pkg/k8s/logging.go b/pkg/k8s/logging.go new file mode 100644 index 00000000..6a608fbb --- /dev/null +++ b/pkg/k8s/logging.go @@ -0,0 +1,31 @@ +// Copyright 2019 HAProxy Technologies LLC +// +// 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 k8s + +import ( + k8smeta "github.com/haproxytech/kubernetes-ingress/pkg/k8s/meta" + "github.com/haproxytech/kubernetes-ingress/pkg/utils" + "k8s.io/apimachinery/pkg/types" +) + +func logIncomingK8sEvent(logger utils.Logger, meta k8smeta.MetaInfoer, uid types.UID, resourceVersion string, additionalInfo ...string) { + logger.Tracef("[RUNTIME] [K8s] %s %s: %s %s/%s ", + meta.GetType(), + meta.GetStatus(), + meta.GetName(), + uid, + resourceVersion, + additionalInfo) +} diff --git a/pkg/k8s/main.go b/pkg/k8s/main.go index 36f04f48..ea03f93f 100644 --- a/pkg/k8s/main.go +++ b/pkg/k8s/main.go @@ -131,6 +131,7 @@ func New(osArgs utils.OSArgs, whitelist map[string]struct{}, publishSvc *utils.N gatewayRestClient: gatewayRestClient, crdClient: crdClient, } + // alpha2 is deprecated k.registerCoreCR(NewGlobalCRV1Alpha2(), CRSGroupVersionV1alpha2) k.registerCoreCR(NewDefaultsCRV1Alpha2(), CRSGroupVersionV1alpha2) diff --git a/pkg/k8s/meta/meta.go b/pkg/k8s/meta/meta.go new file mode 100644 index 00000000..333dec53 --- /dev/null +++ b/pkg/k8s/meta/meta.go @@ -0,0 +1,26 @@ +// Copyright 2019 HAProxy Technologies LLC +// +// 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 meta + +import ( + k8ssync "github.com/haproxytech/kubernetes-ingress/pkg/k8s/sync" +) + +type MetaInfoer interface { + GetNamespace() string + GetName() string + GetType() k8ssync.SyncType + GetStatus() string +} diff --git a/pkg/k8s/meta/store.go b/pkg/k8s/meta/store.go new file mode 100644 index 00000000..d0f1d894 --- /dev/null +++ b/pkg/k8s/meta/store.go @@ -0,0 +1,115 @@ +// Copyright 2019 HAProxy Technologies LLC +// +// 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 meta + +import ( + "fmt" + "sync" + + "github.com/haproxytech/kubernetes-ingress/pkg/utils" + "k8s.io/apimachinery/pkg/types" +) + +var ( + metaInfoStore *MetaStore + metaInfoStoreOnce sync.Once +) + +type ProcessedResourceVersionSafe struct { + // key are and // + // value is resourceVersion + processedResourceVersion map[string]map[string]string + sync.RWMutex + logger utils.Logger + getKeyFunc func(MetaInfoer, types.UID) string +} + +type MetaStore struct { + ProcessedResourceVersion ProcessedResourceVersionSafe +} + +func GetMetaStore() *MetaStore { + metaInfoStoreOnce.Do(func() { + metaInfoStore = &MetaStore{ + ProcessedResourceVersion: ProcessedResourceVersionSafe{ + processedResourceVersion: make(map[string]map[string]string), + logger: utils.GetK8sLogger(), + getKeyFunc: getKey, + }, + } + }) + return metaInfoStore +} + +func (rv *ProcessedResourceVersionSafe) IsProcessed(meta MetaInfoer, uid types.UID, resourceVersion string) bool { + rv.RLock() + defer rv.RUnlock() + key := rv.getKeyFunc(meta, uid) + var v string + var ok bool + objType := string(meta.GetType()) + if _, ok = rv.processedResourceVersion[objType]; !ok { + return false + } + if v, ok = rv.processedResourceVersion[objType][key]; !ok { + return false + } + // By safety, if uid or resourceVersion are empty, consider it as not processed + if uid == "" || resourceVersion == "" { + rv.logger.Tracef("uid or resourceVersion is empty for %s", rv.getKeyFunc(meta, uid)) + return false + } + return v == resourceVersion +} + +func (rv *ProcessedResourceVersionSafe) Set(meta MetaInfoer, uid types.UID, resourceVersion string) { + rv.Lock() + defer rv.Unlock() + + // By safety, if uid or resourceVersion are empty, do not store + if uid == "" || resourceVersion == "" { + rv.logger.Tracef("uid or resourceVersion is empty for %s", rv.getKeyFunc(meta, uid)) + return + } + objType := string(meta.GetType()) + if _, ok := rv.processedResourceVersion[objType]; !ok { + rv.processedResourceVersion[objType] = make(map[string]string) + } + rv.processedResourceVersion[objType][rv.getKeyFunc(meta, uid)] = resourceVersion +} + +func (rv *ProcessedResourceVersionSafe) Delete(meta MetaInfoer, uid types.UID) { + rv.Lock() + defer rv.Unlock() + objType := string(meta.GetType()) + if _, ok := rv.processedResourceVersion[objType]; !ok { + return + } + delete(rv.processedResourceVersion[objType], rv.getKeyFunc(meta, uid)) +} + +func getKey(meta MetaInfoer, uid types.UID) string { + return string(uid) +} + +func getKeyTestMode(meta MetaInfoer, uid types.UID) string { + return fmt.Sprintf("%s/%s/%s", meta.GetNamespace(), meta.GetName(), uid) +} + +func (rv *ProcessedResourceVersionSafe) SetTestMode() { + rv.Lock() + defer rv.Unlock() + rv.getKeyFunc = getKeyTestMode +} diff --git a/pkg/k8s/sync/sync.go b/pkg/k8s/sync/sync.go index cca167ce..acefd42c 100644 --- a/pkg/k8s/sync/sync.go +++ b/pkg/k8s/sync/sync.go @@ -14,6 +14,8 @@ package k8ssync +import "k8s.io/apimachinery/pkg/types" + // SyncType represents type of k8s received message type SyncType string @@ -23,8 +25,10 @@ type SyncDataEvent struct { Data interface{} EventProcessed chan struct{} SyncType - Namespace string - Name string + Namespace string + Name string + UID types.UID + ResourceVersion string } //nolint:golint,stylecheck diff --git a/pkg/k8s/transform/transform-common.go b/pkg/k8s/transform/transform-common.go new file mode 100644 index 00000000..e7d1b7b1 --- /dev/null +++ b/pkg/k8s/transform/transform-common.go @@ -0,0 +1,30 @@ +// Copyright 2019 HAProxy Technologies LLC +// +// 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 k8stransform + +import ( + ammeta "k8s.io/apimachinery/pkg/api/meta" +) + +func TransformCommon(obj interface{}) (interface{}, error) { + data, err := ammeta.Accessor(obj) + if err != nil { + return obj, err + } + + data.SetManagedFields(nil) + + return data, nil +} diff --git a/pkg/k8s/transform/transform-configmap.go b/pkg/k8s/transform/transform-configmap.go new file mode 100644 index 00000000..1d740742 --- /dev/null +++ b/pkg/k8s/transform/transform-configmap.go @@ -0,0 +1,19 @@ +// Copyright 2019 HAProxy Technologies LLC +// +// 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 k8stransform + +func TransformConfigmap(obj interface{}) (interface{}, error) { + return TransformCommon(obj) +} diff --git a/pkg/k8s/transform/transform-endpoints.go b/pkg/k8s/transform/transform-endpoints.go new file mode 100644 index 00000000..79a0efa2 --- /dev/null +++ b/pkg/k8s/transform/transform-endpoints.go @@ -0,0 +1,19 @@ +// Copyright 2019 HAProxy Technologies LLC +// +// 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 k8stransform + +func TransformEndpoints(obj interface{}) (interface{}, error) { + return TransformCommon(obj) +} diff --git a/pkg/k8s/transform/transform-ingress.go b/pkg/k8s/transform/transform-ingress.go new file mode 100644 index 00000000..d29d6d52 --- /dev/null +++ b/pkg/k8s/transform/transform-ingress.go @@ -0,0 +1,131 @@ +// Copyright 2019 HAProxy Technologies LLC +// +// 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 k8stransform + +import ( + "sort" + + "github.com/haproxytech/kubernetes-ingress/pkg/utils" + networkingv1 "k8s.io/api/networking/v1" + "sigs.k8s.io/yaml" +) + +func TransformIngress(obj interface{}) (interface{}, error) { + ing, ok := obj.(*networkingv1.Ingress) + if !ok { + return obj, nil + } + + // Fields to remove + ing.Labels = nil + ing.ObjectMeta.ManagedFields = nil + + // Remove duplicates + RemoveDuplicates(ing) + + return ing, nil +} + +func RemoveDuplicates(ingress *networkingv1.Ingress) { + tls1, containsDups1 := RemoveIngressTLSDuplicates(ingress.Spec.TLS) + rules1, containsDups2 := RemoveIngressRuleDuplicates(ingress.Spec.Rules) + logger := utils.GetK8sLogger() + if containsDups1 || containsDups2 { + // originalValue, _ := yaml.Marshal(ingress) + logger.Warningf("[K8s duplicate] Ingress %s/%s contains dups. Removing them. Consider cleaning your ingress. Original value: %+v", + ingress.Name, + ingress.Namespace, + ingress) + } + + ingress.Spec.TLS = tls1 + ingress.Spec.Rules = rules1 +} + +func RemoveIngressRuleDuplicates(rules []networkingv1.IngressRule) ([]networkingv1.IngressRule, bool) { + if rules == nil { + return nil, false + } + containsDups := false + var result []networkingv1.IngressRule + // map key will be: / + seen := make(map[string]struct{}) + for _, rule := range rules { + if _, ok := seen[rule.Host]; !ok { + jRule, _ := yaml.Marshal(rule.IngressRuleValue) + key := rule.Host + "/" + utils.Hash(jRule) + if _, ok := seen[key]; ok { + containsDups = true + continue + } + seen[key] = struct{}{} + result = append(result, rule) + } + } + + return result, containsDups +} + +func RemoveIngressTLSDuplicates(tlsList []networkingv1.IngressTLS) ([]networkingv1.IngressTLS, bool) { + if tlsList == nil { + return nil, false + } + + containsDups := false + + tlsWithoutHostDupls := make([]networkingv1.IngressTLS, 0) + + // First step, clean each tls.hosts by removing duplicates inside the hosts. + for _, ingtls := range tlsList { + // if max 1 host, nothing to do,no dups + if len(ingtls.Hosts) <= 1 { + tlsWithoutHostDupls = append(tlsWithoutHostDupls, ingtls) + continue + } + // If multiple hosts, remove dups if there are some + distinctHosts := map[string]struct{}{} + for _, host := range ingtls.Hosts { + distinctHosts[host] = struct{}{} + } + newHosts := make([]string, 0, len(distinctHosts)) + for host := range distinctHosts { + newHosts = append(newHosts, host) + } + if len(newHosts) != len(ingtls.Hosts) { + containsDups = true + } + sort.Strings(newHosts) + ingtls.Hosts = newHosts + tlsWithoutHostDupls = append(tlsWithoutHostDupls, ingtls) + } + + result := make([]networkingv1.IngressTLS, 0) + + // Then remove duplicates among TLS themselves + // map key will be: /<(hash(hosts))> + distinctTLSMap := map[string]struct{}{} + for _, ingtls := range tlsWithoutHostDupls { + jTLS, _ := yaml.Marshal(ingtls) + key := ingtls.SecretName + "/" + utils.Hash(jTLS) + if _, ok := distinctTLSMap[key]; ok { + containsDups = true + continue + } + result = append(result, ingtls) + distinctTLSMap[key] = struct{}{} + } + + return result, containsDups +} diff --git a/pkg/k8s/transform/transform-ingressclass.go b/pkg/k8s/transform/transform-ingressclass.go new file mode 100644 index 00000000..d0a26c78 --- /dev/null +++ b/pkg/k8s/transform/transform-ingressclass.go @@ -0,0 +1,19 @@ +// Copyright 2019 HAProxy Technologies LLC +// +// 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 k8stransform + +func TransformIngressClass(obj interface{}) (interface{}, error) { + return TransformCommon(obj) +} diff --git a/pkg/k8s/transform/transform-namespace.go b/pkg/k8s/transform/transform-namespace.go new file mode 100644 index 00000000..074706bb --- /dev/null +++ b/pkg/k8s/transform/transform-namespace.go @@ -0,0 +1,19 @@ +// Copyright 2019 HAProxy Technologies LLC +// +// 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 k8stransform + +func TransformNamespace(obj interface{}) (interface{}, error) { + return TransformCommon(obj) +} diff --git a/pkg/k8s/transform/transform-secret.go b/pkg/k8s/transform/transform-secret.go new file mode 100644 index 00000000..f2a30c15 --- /dev/null +++ b/pkg/k8s/transform/transform-secret.go @@ -0,0 +1,19 @@ +// Copyright 2019 HAProxy Technologies LLC +// +// 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 k8stransform + +func TransformSecret(obj interface{}) (interface{}, error) { + return TransformCommon(obj) +} diff --git a/pkg/k8s/transform/transform-service.go b/pkg/k8s/transform/transform-service.go new file mode 100644 index 00000000..dbf6d9a3 --- /dev/null +++ b/pkg/k8s/transform/transform-service.go @@ -0,0 +1,19 @@ +// Copyright 2019 HAProxy Technologies LLC +// +// 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 k8stransform + +func TransformService(obj interface{}) (interface{}, error) { + return TransformCommon(obj) +} diff --git a/pkg/store/convert.go b/pkg/store/convert.go index ecaf2cdb..bd711ea6 100644 --- a/pkg/store/convert.go +++ b/pkg/store/convert.go @@ -19,6 +19,8 @@ import ( "strings" networkingv1 "k8s.io/api/networking/v1" + ammeta "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/types" ) //nolint:golint,stylecheck @@ -42,6 +44,26 @@ func ConvertToIngress(resource interface{}) (ingress *Ingress, err error) { return } +func GetIngress(resource interface{}) (ingress *networkingv1.Ingress, err error) { + switch t := resource.(type) { + case *networkingv1.Ingress: + return resource.(*networkingv1.Ingress), nil + default: + err = fmt.Errorf("unrecognized type for: %T", t) + } + return +} + +func GetUIDResourceVersion(resource interface{}) (uid types.UID, resourceVersion string, err error) { + v, err := ammeta.Accessor(resource) + if err != nil { + return + } + uid = v.GetUID() + resourceVersion = v.GetResourceVersion() + return +} + func ConvertToIngressClass(resource interface{}) (ingress *IngressClass, err error) { switch t := resource.(type) { case *networkingv1.IngressClass: diff --git a/pkg/store/events.go b/pkg/store/events.go index c1a38909..09f396ef 100644 --- a/pkg/store/events.go +++ b/pkg/store/events.go @@ -18,7 +18,9 @@ import ( "strings" "github.com/go-test/deep" + "github.com/haproxytech/kubernetes-ingress/pkg/k8s/meta" "github.com/haproxytech/kubernetes-ingress/pkg/utils" + "k8s.io/apimachinery/pkg/types" ) func (k *K8s) EventNamespace(ns *Namespace, data *Namespace) (updateRequired bool) { @@ -57,11 +59,12 @@ func (k *K8s) EventIngressClass(data *IngressClass) (updateRequired bool) { return } -func (k *K8s) EventIngress(ns *Namespace, data *Ingress) (updateRequired bool) { +func (k *K8s) EventIngress(ns *Namespace, data *Ingress, uid types.UID, resourceVersion string) (updateRequired bool) { updateRequired = true if data.Status == DELETED { delete(ns.Ingresses, data.Name) + meta.GetMetaStore().ProcessedResourceVersion.Delete(data, uid) } else { if oldIngress, ok := ns.Ingresses[data.Name]; ok { updated := deep.Equal(data.IngressCore, oldIngress.IngressCore) @@ -80,6 +83,7 @@ func (k *K8s) EventIngress(ns *Namespace, data *Ingress) (updateRequired bool) { } } ns.Ingresses[data.Name] = data + meta.GetMetaStore().ProcessedResourceVersion.Set(data, uid, resourceVersion) } return } diff --git a/pkg/store/types-configmap.go b/pkg/store/types-configmap.go new file mode 100644 index 00000000..cce1eb47 --- /dev/null +++ b/pkg/store/types-configmap.go @@ -0,0 +1,35 @@ +// Copyright 2019 HAProxy Technologies LLC +// +// 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 store + +import ( + k8ssync "github.com/haproxytech/kubernetes-ingress/pkg/k8s/sync" +) + +func (a ConfigMap) GetType() k8ssync.SyncType { + return k8ssync.CONFIGMAP +} + +func (a ConfigMap) GetName() string { + return a.Name +} + +func (a ConfigMap) GetNamespace() string { + return a.Namespace +} + +func (a ConfigMap) GetStatus() string { + return string(a.Status) +} diff --git a/pkg/store/types-endpoints.go b/pkg/store/types-endpoints.go new file mode 100644 index 00000000..b2409499 --- /dev/null +++ b/pkg/store/types-endpoints.go @@ -0,0 +1,35 @@ +// Copyright 2019 HAProxy Technologies LLC +// +// 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 store + +import ( + k8ssync "github.com/haproxytech/kubernetes-ingress/pkg/k8s/sync" +) + +func (a Endpoints) GetType() k8ssync.SyncType { + return k8ssync.ENDPOINTS +} + +func (a Endpoints) GetName() string { + return a.SliceName +} + +func (a Endpoints) GetNamespace() string { + return a.Namespace +} + +func (a Endpoints) GetStatus() string { + return string(a.Status) +} diff --git a/pkg/store/types-ingress.go b/pkg/store/types-ingress.go new file mode 100644 index 00000000..ccf3c246 --- /dev/null +++ b/pkg/store/types-ingress.go @@ -0,0 +1,35 @@ +// Copyright 2019 HAProxy Technologies LLC +// +// 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 store + +import ( + k8ssync "github.com/haproxytech/kubernetes-ingress/pkg/k8s/sync" +) + +func (i Ingress) GetType() k8ssync.SyncType { + return k8ssync.INGRESS +} + +func (i Ingress) GetName() string { + return i.Name +} + +func (i Ingress) GetNamespace() string { + return i.Namespace +} + +func (i Ingress) GetStatus() string { + return string(i.Status) +} diff --git a/pkg/store/types-ingressclass.go b/pkg/store/types-ingressclass.go new file mode 100644 index 00000000..9b10f6d7 --- /dev/null +++ b/pkg/store/types-ingressclass.go @@ -0,0 +1,35 @@ +// Copyright 2019 HAProxy Technologies LLC +// +// 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 store + +import ( + k8ssync "github.com/haproxytech/kubernetes-ingress/pkg/k8s/sync" +) + +func (a IngressClass) GetType() k8ssync.SyncType { + return k8ssync.INGRESS_CLASS +} + +func (a IngressClass) GetName() string { + return a.Name +} + +func (a IngressClass) GetNamespace() string { + return "" +} + +func (a IngressClass) GetStatus() string { + return string(a.Status) +} diff --git a/pkg/store/types-namespace.go b/pkg/store/types-namespace.go new file mode 100644 index 00000000..3883d84c --- /dev/null +++ b/pkg/store/types-namespace.go @@ -0,0 +1,35 @@ +// Copyright 2019 HAProxy Technologies LLC +// +// 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 store + +import ( + k8ssync "github.com/haproxytech/kubernetes-ingress/pkg/k8s/sync" +) + +func (ns Namespace) GetType() k8ssync.SyncType { + return k8ssync.NAMESPACE +} + +func (ns Namespace) GetName() string { + return ns.Name +} + +func (ns Namespace) GetNamespace() string { + return ns.Name +} + +func (ns Namespace) GetStatus() string { + return string(ns.Status) +} diff --git a/pkg/store/types-pod.go b/pkg/store/types-pod.go new file mode 100644 index 00000000..919aba70 --- /dev/null +++ b/pkg/store/types-pod.go @@ -0,0 +1,35 @@ +// Copyright 2019 HAProxy Technologies LLC +// +// 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 store + +import ( + k8ssync "github.com/haproxytech/kubernetes-ingress/pkg/k8s/sync" +) + +func (i PodEvent) GetType() k8ssync.SyncType { + return k8ssync.POD +} + +func (i PodEvent) GetName() string { + return i.Name +} + +func (i PodEvent) GetNamespace() string { + return i.Namespace +} + +func (i PodEvent) GetStatus() string { + return string(i.Status) +} diff --git a/pkg/store/types-secret.go b/pkg/store/types-secret.go new file mode 100644 index 00000000..f05d2fdf --- /dev/null +++ b/pkg/store/types-secret.go @@ -0,0 +1,35 @@ +// Copyright 2019 HAProxy Technologies LLC +// +// 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 store + +import ( + k8ssync "github.com/haproxytech/kubernetes-ingress/pkg/k8s/sync" +) + +func (a Secret) GetType() k8ssync.SyncType { + return k8ssync.SECRET +} + +func (a Secret) GetName() string { + return a.Name +} + +func (a Secret) GetNamespace() string { + return a.Namespace +} + +func (a Secret) GetStatus() string { + return string(a.Status) +} diff --git a/pkg/store/types-service.go b/pkg/store/types-service.go new file mode 100644 index 00000000..55e79f62 --- /dev/null +++ b/pkg/store/types-service.go @@ -0,0 +1,35 @@ +// Copyright 2019 HAProxy Technologies LLC +// +// 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 store + +import ( + k8ssync "github.com/haproxytech/kubernetes-ingress/pkg/k8s/sync" +) + +func (a Service) GetType() k8ssync.SyncType { + return k8ssync.SERVICE +} + +func (a Service) GetName() string { + return a.Name +} + +func (a Service) GetNamespace() string { + return a.Namespace +} + +func (a Service) GetStatus() string { + return string(a.Status) +} diff --git a/pkg/store/types.go b/pkg/store/types.go index 69559833..8d7d64bf 100644 --- a/pkg/store/types.go +++ b/pkg/store/types.go @@ -59,8 +59,9 @@ type Endpoints struct { // PodEvent carries creation/deletion pod event. type PodEvent struct { - Status Status - Name string + Status Status + Name string + Namespace string } // Service is useful data from k8s structures about service diff --git a/test/transform/data/ingress/dupl-1.yaml b/test/transform/data/ingress/dupl-1.yaml new file mode 100644 index 00000000..f92165c0 --- /dev/null +++ b/test/transform/data/ingress/dupl-1.yaml @@ -0,0 +1,57 @@ +kind: Ingress +apiVersion: networking.k8s.io/v1 +metadata: + name: testingress + annotations: + ingress.class: haproxy +spec: + tls: + - hosts: + - test1.haproxy + - test1.haproxy + - test1.haproxy + secretName: test1 + - hosts: + - test2.haproxy + secretName: test2 + rules: + - host: "test.haproxy" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: foo1 + port: + name: http + - host: "test.haproxy" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: foo1 + port: + name: http + - host: "test.haproxy" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: foo1 + port: + name: http + - host: "test2.haproxy" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: foo2 + port: + name: http diff --git a/test/transform/data/ingress/expectation/no-duplicate.yaml b/test/transform/data/ingress/expectation/no-duplicate.yaml new file mode 100644 index 00000000..c4b91531 --- /dev/null +++ b/test/transform/data/ingress/expectation/no-duplicate.yaml @@ -0,0 +1,35 @@ +kind: Ingress +apiVersion: networking.k8s.io/v1 +metadata: + name: testingress + annotations: + ingress.class: haproxy +spec: + tls: + - hosts: + - test1.haproxy + secretName: test1 + - hosts: + - test2.haproxy + secretName: test2 + rules: + - host: "test.haproxy" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: foo1 + port: + name: http + - host: "test2.haproxy" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: foo2 + port: + name: http diff --git a/test/transform/data/ingress/no-duplicate.yaml b/test/transform/data/ingress/no-duplicate.yaml new file mode 100644 index 00000000..c4b91531 --- /dev/null +++ b/test/transform/data/ingress/no-duplicate.yaml @@ -0,0 +1,35 @@ +kind: Ingress +apiVersion: networking.k8s.io/v1 +metadata: + name: testingress + annotations: + ingress.class: haproxy +spec: + tls: + - hosts: + - test1.haproxy + secretName: test1 + - hosts: + - test2.haproxy + secretName: test2 + rules: + - host: "test.haproxy" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: foo1 + port: + name: http + - host: "test2.haproxy" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: foo2 + port: + name: http diff --git a/test/transform/data/rule/dupl-1.yaml b/test/transform/data/rule/dupl-1.yaml new file mode 100644 index 00000000..43b2aa9a --- /dev/null +++ b/test/transform/data/rule/dupl-1.yaml @@ -0,0 +1,31 @@ +--- +- host: "test.haproxy" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: foo1 + port: + name: http +- host: "test.haproxy" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: foo1 + port: + name: http +- host: "test2.haproxy" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: foo2 + port: + name: http diff --git a/test/transform/data/rule/expectation/no-duplicate.yaml b/test/transform/data/rule/expectation/no-duplicate.yaml new file mode 100644 index 00000000..44f97774 --- /dev/null +++ b/test/transform/data/rule/expectation/no-duplicate.yaml @@ -0,0 +1,21 @@ +--- +- host: "test.haproxy" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: foo1 + port: + name: http +- host: "test2.haproxy" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: foo2 + port: + name: http diff --git a/test/transform/data/rule/no-dupl-1.yaml b/test/transform/data/rule/no-dupl-1.yaml new file mode 100644 index 00000000..4202995f --- /dev/null +++ b/test/transform/data/rule/no-dupl-1.yaml @@ -0,0 +1,31 @@ +--- +- host: "test.haproxy" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: foo1 + port: + name: http +- host: "test.haproxy" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: foo111 ### It's not the same rule, it should stay + port: + name: http +- host: "test2.haproxy" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: foo2 + port: + name: http diff --git a/test/transform/data/rule/no-dupl-2.yaml b/test/transform/data/rule/no-dupl-2.yaml new file mode 100644 index 00000000..107e6845 --- /dev/null +++ b/test/transform/data/rule/no-dupl-2.yaml @@ -0,0 +1,38 @@ +--- +- host: "test.haproxy" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: foo1 + port: + name: http +- host: "test.haproxy" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: foo1 + port: + name: http + - path: /foo12 + pathType: Prefix + backend: + service: + name: foo12 + port: + name: http +- host: "test2.haproxy" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: foo2 + port: + name: http diff --git a/test/transform/data/rule/no-duplicate.yaml b/test/transform/data/rule/no-duplicate.yaml new file mode 100644 index 00000000..44f97774 --- /dev/null +++ b/test/transform/data/rule/no-duplicate.yaml @@ -0,0 +1,21 @@ +--- +- host: "test.haproxy" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: foo1 + port: + name: http +- host: "test2.haproxy" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: foo2 + port: + name: http diff --git a/test/transform/data/tls/dupl-both.yaml b/test/transform/data/tls/dupl-both.yaml new file mode 100644 index 00000000..80b3b248 --- /dev/null +++ b/test/transform/data/tls/dupl-both.yaml @@ -0,0 +1,12 @@ +--- +# tls: +- hosts: + - test1.haproxy + - test1.haproxy + - test1.haproxy + - test1.haproxy + secretName: test1 +- hosts: + - test2.haproxy + - test2.haproxy + secretName: test2 diff --git a/test/transform/data/tls/dupl-hosts.yaml b/test/transform/data/tls/dupl-hosts.yaml new file mode 100644 index 00000000..3aae859c --- /dev/null +++ b/test/transform/data/tls/dupl-hosts.yaml @@ -0,0 +1,11 @@ +--- +# tls: +- hosts: + - test1.haproxy + - test1.haproxy + - test1.haproxy + - test1.haproxy + secretName: test1 +- hosts: + - test2.haproxy + secretName: test2 diff --git a/test/transform/data/tls/dupl-tls-2.yaml b/test/transform/data/tls/dupl-tls-2.yaml new file mode 100644 index 00000000..1e1472a3 --- /dev/null +++ b/test/transform/data/tls/dupl-tls-2.yaml @@ -0,0 +1,29 @@ +--- +# tls: +- hosts: + - test1.haproxy + secretName: test1 +- hosts: + - test1.haproxy + - test111.haproxy + secretName: test1 +- hosts: + - test112.haproxy + secretName: test1 +- hosts: + - test112.haproxy + - test1121.haproxy + secretName: test1 +- hosts: + - test1.haproxy + secretName: test1 +- hosts: + - test1.haproxy + - test111.haproxy + secretName: test1 +- hosts: + - test2.haproxy + secretName: test2 +- hosts: + - test2.haproxy + secretName: test2 diff --git a/test/transform/data/tls/dupl-tls.yaml b/test/transform/data/tls/dupl-tls.yaml new file mode 100644 index 00000000..7966d289 --- /dev/null +++ b/test/transform/data/tls/dupl-tls.yaml @@ -0,0 +1,20 @@ +--- +# tls: +- hosts: + - test1.haproxy + secretName: test1 +- hosts: + - test1.haproxy + secretName: test1 +- hosts: + - test1.haproxy + secretName: test1 +- hosts: + - test1.haproxy + secretName: test1 +- hosts: + - test2.haproxy + secretName: test2 +- hosts: + - test2.haproxy + secretName: test2 diff --git a/test/transform/data/tls/expectation/dupl-tls-2.yaml b/test/transform/data/tls/expectation/dupl-tls-2.yaml new file mode 100644 index 00000000..4483cdd4 --- /dev/null +++ b/test/transform/data/tls/expectation/dupl-tls-2.yaml @@ -0,0 +1,19 @@ +--- +# tls: +- hosts: + - test1.haproxy + secretName: test1 +- hosts: + - test1.haproxy + - test111.haproxy + secretName: test1 +- hosts: + - test112.haproxy + secretName: test1 +- hosts: + - test112.haproxy + - test1121.haproxy + secretName: test1 +- hosts: + - test2.haproxy + secretName: test2 diff --git a/test/transform/data/tls/expectation/no-duplicate.yaml b/test/transform/data/tls/expectation/no-duplicate.yaml new file mode 100644 index 00000000..450a32f8 --- /dev/null +++ b/test/transform/data/tls/expectation/no-duplicate.yaml @@ -0,0 +1,8 @@ +--- +# tls: +- hosts: + - test1.haproxy + secretName: test1 +- hosts: + - test2.haproxy + secretName: test2 diff --git a/test/transform/data/tls/no-duplicate.yaml b/test/transform/data/tls/no-duplicate.yaml new file mode 100644 index 00000000..a89481bb --- /dev/null +++ b/test/transform/data/tls/no-duplicate.yaml @@ -0,0 +1,8 @@ +--- +# tls: +- hosts: + - test1.haproxy + secretName: test1 +- hosts: + - test2.haproxy + secretName: test2 diff --git a/test/transform/transform-ingress_test.go b/test/transform/transform-ingress_test.go new file mode 100644 index 00000000..49050dda --- /dev/null +++ b/test/transform/transform-ingress_test.go @@ -0,0 +1,193 @@ +// Copyright 2019 HAProxy Technologies LLC +// +// 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 k8stransform_test + +import ( + "os" + "testing" + + k8stransform "github.com/haproxytech/kubernetes-ingress/pkg/k8s/transform" + "github.com/stretchr/testify/require" + "sigs.k8s.io/yaml" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + + networkingv1 "k8s.io/api/networking/v1" +) + +func TestRemoveIngressDuplicates(t *testing.T) { + tests := []struct { + name string + inputPath string + expectedPath string + }{ + { + name: "no duplicates", + inputPath: "data/ingress/no-duplicate.yaml", + expectedPath: "data/ingress/expectation/no-duplicate.yaml", + }, + { + name: "dupl-1", + inputPath: "data/ingress/dupl-1.yaml", + expectedPath: "data/ingress/expectation/no-duplicate.yaml", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ingress := getIngressFromManifest(t, tt.inputPath) + k8stransform.RemoveDuplicates(ingress) + + expectedIngress := getIngressFromManifest(t, tt.expectedPath) + sortHost := cmpopts.SortSlices(func(a, b string) bool { + return a < b + }) + if !cmp.Equal(ingress, expectedIngress, sortHost) { + diff := cmp.Diff(ingress, expectedIngress, sortHost) + t.Errorf("[%s] ingress does not match expectation\n%v", tt.name, diff) + } + }) + } +} + +func TestRemoveIngressTLSDuplicates(t *testing.T) { + tests := []struct { + name string + inputPath string + expectedPath string + hasDups bool + }{ + { + name: "no duplicates", + inputPath: "data/tls/no-duplicate.yaml", + expectedPath: "data/tls/expectation/no-duplicate.yaml", + }, + { + name: "multiple hosts same tls", + inputPath: "data/tls/dupl-hosts.yaml", + expectedPath: "data/tls/expectation/no-duplicate.yaml", + hasDups: true, + }, + { + name: "multiple hosts same tls", + inputPath: "data/tls/dupl-tls.yaml", + expectedPath: "data/tls/expectation/no-duplicate.yaml", + hasDups: true, + }, + { + name: "multiple hosts same tls-2", + inputPath: "data/tls/dupl-tls-2.yaml", + expectedPath: "data/tls/expectation/dupl-tls-2.yaml", + hasDups: true, + }, + { + name: "multiple hosts", + inputPath: "data/tls/dupl-both.yaml", + expectedPath: "data/tls/expectation/no-duplicate.yaml", + hasDups: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tls := getTLSFromManifest(t, tt.inputPath) + got, hasDups := k8stransform.RemoveIngressTLSDuplicates(tls) + + expectedTLS := getTLSFromManifest(t, tt.expectedPath) + sortHost := cmpopts.SortSlices(func(a, b string) bool { + return a < b + }) + if !cmp.Equal(got, expectedTLS, sortHost) { + diff := cmp.Diff(got, expectedTLS, sortHost) + t.Errorf("[%s] tls does not match expectation\n%v", tt.name, diff) + } + if hasDups != tt.hasDups { + t.Errorf("[%s] hasDups got [%t] want [%t]", tt.name, hasDups, tt.hasDups) + } + }) + } +} + +func TestRemoveIngressRuleDuplicates(t *testing.T) { + tests := []struct { + name string + inputPath string + expectedPath string + hasDups bool + }{ + { + name: "no duplicates", + inputPath: "data/rule/no-duplicate.yaml", + expectedPath: "data/rule/expectation/no-duplicate.yaml", + }, + { + name: "dupl-1", + inputPath: "data/rule/dupl-1.yaml", + expectedPath: "data/rule/expectation/no-duplicate.yaml", + hasDups: true, + }, + { + name: "no dupl-1", + inputPath: "data/rule/no-dupl-1.yaml", + expectedPath: "data/rule/no-dupl-1.yaml", + }, + { + name: "no dupl-2", + inputPath: "data/rule/no-dupl-2.yaml", + expectedPath: "data/rule/no-dupl-2.yaml", // same as input + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rules := getRuleFromManifest(t, tt.inputPath) + got, hasDups := k8stransform.RemoveIngressRuleDuplicates(rules) + + expectedRules := getRuleFromManifest(t, tt.expectedPath) + if !cmp.Equal(got, expectedRules) { + diff := cmp.Diff(got, expectedRules) + t.Errorf("[%s] rule does not match expectation \n%v\ngot %v\nwant %v", tt.name, diff, got, expectedRules) + } + if hasDups != tt.hasDups { + t.Errorf("[%s] hasDups got [%t] want [%t]", tt.name, hasDups, tt.hasDups) + } + }) + } +} + +func getIngressFromManifest(t *testing.T, path string) *networkingv1.Ingress { + t.Helper() + jIng, err := os.ReadFile(path) + require.NoError(t, err) + var ing networkingv1.Ingress + _ = yaml.Unmarshal(jIng, &ing) + return &ing +} + +func getRuleFromManifest(t *testing.T, path string) []networkingv1.IngressRule { + t.Helper() + jTLS, err := os.ReadFile(path) + require.NoError(t, err) + var rules []networkingv1.IngressRule + _ = yaml.Unmarshal(jTLS, &rules) + return rules +} + +func getTLSFromManifest(t *testing.T, path string) []networkingv1.IngressTLS { + t.Helper() + jTLS, err := os.ReadFile(path) + require.NoError(t, err) + var tls []networkingv1.IngressTLS + _ = yaml.Unmarshal(jTLS, &tls) + return tls +}