From fff2da0d66039e6a2ac82bbe4f4e1452c7f48b22 Mon Sep 17 00:00:00 2001 From: davidefalcone1 Date: Tue, 8 Jun 2021 11:30:25 +0200 Subject: [PATCH] New functions for NatMappingInflater module Added AddMapping and TerminateNatMappingsPerCluster functions. --- apis/net/v1alpha1/ipamstorage_types.go | 7 + apis/net/v1alpha1/zz_generated.deepcopy.go | 22 ++ .../liqo/crds/net.liqo.io_ipamstorages.yaml | 10 + .../tunnelEndpointCreator-operator.go | 4 +- pkg/liqonet/ipam.go | 190 ++++++++--- pkg/liqonet/ipamStorage.go | 40 ++- pkg/liqonet/ipam_test.go | 296 ++++++++++++++++-- .../natmappinginflater/natMappingInflater.go | 116 +++++++ .../natMappingInflater_test.go | 68 ++++ 9 files changed, 677 insertions(+), 76 deletions(-) diff --git a/apis/net/v1alpha1/ipamstorage_types.go b/apis/net/v1alpha1/ipamstorage_types.go index fb4c35412d..b2ac745c7d 100644 --- a/apis/net/v1alpha1/ipamstorage_types.go +++ b/apis/net/v1alpha1/ipamstorage_types.go @@ -38,6 +38,9 @@ type Subnets struct { // ClusterMapping is an empty struct. type ClusterMapping struct{} +// ConfiguredCluster is an empty struct used as value for NatMappingsConfigured. +type ConfiguredCluster struct{} + // EndpointMapping describes a relation between an enpoint IP and an IP belonging to ExternalCIDR. type EndpointMapping struct { // IP belonging to cluster ExtenalCIDR assigned to this endpoint. @@ -61,6 +64,10 @@ type IpamSpec struct { ExternalCIDR string `json:"externalCIDR"` // Endpoint IP mappings. Key is the IP address of the local endpoint, value is the IP of the remote endpoint, so it belongs to an ExternalCIDR EndpointMappings map[string]EndpointMapping `json:"endpointMappings"` + // NatMappingsConfigured is a map that contains all the remote clusters + // for which NatMappings have been already configured. + // Key is a cluster ID, value is an empty struct. + NatMappingsConfigured map[string]ConfiguredCluster `json:"natMappingsConfigured"` // Cluster PodCIDR PodCIDR string `json:"podCIDR"` // ServiceCIDR diff --git a/apis/net/v1alpha1/zz_generated.deepcopy.go b/apis/net/v1alpha1/zz_generated.deepcopy.go index ed8009d6a6..fe5439f80f 100644 --- a/apis/net/v1alpha1/zz_generated.deepcopy.go +++ b/apis/net/v1alpha1/zz_generated.deepcopy.go @@ -38,6 +38,21 @@ func (in *ClusterMapping) DeepCopy() *ClusterMapping { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConfiguredCluster) DeepCopyInto(out *ConfiguredCluster) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfiguredCluster. +func (in *ConfiguredCluster) DeepCopy() *ConfiguredCluster { + if in == nil { + return nil + } + out := new(ConfiguredCluster) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Connection) DeepCopyInto(out *Connection) { *out = *in @@ -119,6 +134,13 @@ func (in *IpamSpec) DeepCopyInto(out *IpamSpec) { (*out)[key] = *val.DeepCopy() } } + if in.NatMappingsConfigured != nil { + in, out := &in.NatMappingsConfigured, &out.NatMappingsConfigured + *out = make(map[string]ConfiguredCluster, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IpamSpec. diff --git a/deployments/liqo/crds/net.liqo.io_ipamstorages.yaml b/deployments/liqo/crds/net.liqo.io_ipamstorages.yaml index afb4245b61..31bf7ecada 100644 --- a/deployments/liqo/crds/net.liqo.io_ipamstorages.yaml +++ b/deployments/liqo/crds/net.liqo.io_ipamstorages.yaml @@ -95,6 +95,15 @@ spec: externalCIDR: description: Cluster ExternalCIDR type: string + natMappingsConfigured: + additionalProperties: + description: ConfiguredCluster is an empty struct used as value + for NatMappingsConfigured. + type: object + description: NatMappingsConfigured is a map that contains all the + remote clusters for which NatMappings have been already configured. + Key is a cluster ID, value is an empty struct. + type: object podCIDR: description: Cluster PodCIDR type: string @@ -118,6 +127,7 @@ spec: - clusterSubnets - endpointMappings - externalCIDR + - natMappingsConfigured - podCIDR - pools - prefixes diff --git a/internal/liqonet/tunnelEndpointCreator/tunnelEndpointCreator-operator.go b/internal/liqonet/tunnelEndpointCreator/tunnelEndpointCreator-operator.go index 3473b663ac..9be3898cc4 100644 --- a/internal/liqonet/tunnelEndpointCreator/tunnelEndpointCreator-operator.go +++ b/internal/liqonet/tunnelEndpointCreator/tunnelEndpointCreator-operator.go @@ -224,8 +224,8 @@ func (tec *TunnelEndpointCreator) SetupSignalHandlerForTunEndCreator() context.C go func(r *TunnelEndpointCreator) { sig := <-c klog.Infof("received signal: %s", sig.String()) - // Stop IPAM gRPC server. - tec.IPManager.StopGRPCServer() + // Stop IPAM. + tec.IPManager.Terminate() // closing shared informers close(r.ForeignClusterStopWatcher) done() diff --git a/pkg/liqonet/ipam.go b/pkg/liqonet/ipam.go index 1e3bde696f..c5bb5d8e15 100644 --- a/pkg/liqonet/ipam.go +++ b/pkg/liqonet/ipam.go @@ -2,6 +2,7 @@ package liqonet import ( "context" + "errors" "fmt" "net" "strings" @@ -31,7 +32,7 @@ type Ipam interface { reserved as well. The remapping mechanism can be applied on: - PodCIDR - ExternalCIDR - - Both + - Both. */ GetSubnetsPerCluster(podCidr, externalCIDR, clusterID string) (string, string, error) // RemoveClusterConfig deletes the IPAM configuration of a remote cluster, @@ -55,8 +56,8 @@ type Ipam interface { SetPodCIDR(podCIDR string) error // SetServiceCIDR sets the cluster ServiceCIDR. SetServiceCIDR(serviceCIDR string) error - // StopGRPCServer stops the gRPC server gracefully. - StopGRPCServer() + // Terminate function enforces a graceful termination of the IPAM module. + Terminate() IpamServer } @@ -122,6 +123,12 @@ func (liqoIPAM *IPAM) Init(pools []string, dynClient dynamic.Interface, listenin return nil } +// Terminate function stops the gRPC server. +func (liqoIPAM *IPAM) Terminate() { + // Stop GRPC server + liqoIPAM.grpcServer.GracefulStop() +} + func (liqoIPAM *IPAM) initRPCServer(port int) error { lis, err := net.Listen("tcp", fmt.Sprintf("%s%d", "0.0.0.0:", port)) if err != nil { @@ -138,11 +145,6 @@ func (liqoIPAM *IPAM) initRPCServer(port int) error { return nil } -// StopGRPCServer stops the gRPC server gracefully. -func (liqoIPAM *IPAM) StopGRPCServer() { - liqoIPAM.grpcServer.GracefulStop() -} - // reservePoolInHalves handles the special case in which a network pool has to be entirely reserved // Since AcquireSpecificChildPrefix would return an error, reservePoolInHalves acquires the two // halves of the network pool. @@ -513,7 +515,7 @@ func (liqoIPAM *IPAM) FreeReservedSubnet(network string) error { // deletes local subnets for the remote cluster. func (liqoIPAM *IPAM) RemoveClusterConfig(clusterID string) error { var subnets netv1alpha1.Subnets - var subnetsExist bool + var subnetsExist, natMappingsPerClusterConfigured bool if clusterID == "" { return &liqoneterrors.WrongParameter{ @@ -528,47 +530,56 @@ func (liqoIPAM *IPAM) RemoveClusterConfig(clusterID string) error { return fmt.Errorf("cannot get cluster subnets: %w", err) } + // Get NatMappingsConfigured map + natMappingsConfigured, err := liqoIPAM.ipamStorage.getNatMappingsConfigured() + if err != nil { + return fmt.Errorf("cannot establish if NatMappings for cluster %s have been configured: %w", clusterID, err) + } + subnets, subnetsExist = clusterSubnets[clusterID] - if !subnetsExist { + _, natMappingsPerClusterConfigured = natMappingsConfigured[clusterID] + if !subnetsExist && !natMappingsPerClusterConfigured { // Nothing to be done here return nil } - // Free PodCidr - if err := liqoIPAM.FreeReservedSubnet(subnets.RemotePodCIDR); err != nil { - return err + // If an error happened after the following if, there is no need of + // re-executing the following block. + if subnetsExist { + // Free PodCidr + if err := liqoIPAM.FreeReservedSubnet(subnets.RemotePodCIDR); err != nil { + return err + } + + // Free ExternalCidr + if err := liqoIPAM.FreeReservedSubnet(subnets.RemoteExternalCIDR); err != nil { + return err + } + klog.Infof("Networks assigned to cluster %s have just been freed", clusterID) + + delete(clusterSubnets, clusterID) + if err := liqoIPAM.ipamStorage.updateClusterSubnets(clusterSubnets); err != nil { + return fmt.Errorf("cannot update clusterSubnets:%w", err) + } } - // Free ExternalCidr - if err := liqoIPAM.FreeReservedSubnet(subnets.RemoteExternalCIDR); err != nil { - return err + // Terminate NatMappings + if err := liqoIPAM.terminateNatMappingsPerCluster(clusterID); err != nil { + return fmt.Errorf("unable to terminate NAT mappings for cluster %s: %w", clusterID, err) } - klog.Infof("Networks assigned to cluster %s have just been freed", clusterID) - delete(clusterSubnets, clusterID) - if err := liqoIPAM.ipamStorage.updateClusterSubnets(clusterSubnets); err != nil { - return fmt.Errorf("cannot update clusterSubnets:%w", err) + delete(natMappingsConfigured, clusterID) + // Update natMappingsConfigured + natMappingsConfigured[clusterID] = netv1alpha1.ConfiguredCluster{} + if err := liqoIPAM.ipamStorage.updateNatMappingsConfigured(natMappingsConfigured); err != nil { + return fmt.Errorf("unable to update NatMappingsConfigured: %w", err) } return nil } // initNatMappingsPerCluster is a wrapper for inflater InitNatMappingsPerCluster. -func (liqoIPAM *IPAM) initNatMappingsPerCluster(clusterID string) error { - if clusterID == "" { - return &liqoneterrors.WrongParameter{ - Parameter: consts.ClusterIDLabelName, - Reason: liqoneterrors.StringNotEmpty, - } - } - // Get cluster subnets - clusterSubnets, err := liqoIPAM.ipamStorage.getClusterSubnets() - if err != nil { - return fmt.Errorf("cannot get cluster subnets: %w", err) - } - subnets, exists := clusterSubnets[clusterID] - if !exists { - return fmt.Errorf("subnets of cluster %s are not set", clusterID) - } +func (liqoIPAM *IPAM) initNatMappingsPerCluster(clusterID string, subnets netv1alpha1.Subnets) error { + var err error // InitNatMappingsPerCluster does need the Pod CIDR used in home cluster for remote pods (subnets.RemotePodCIDR) // and the ExternalCIDR used in remote cluster for local exported resources. var externalCIDR string @@ -584,6 +595,71 @@ func (liqoIPAM *IPAM) initNatMappingsPerCluster(clusterID string) error { return liqoIPAM.natMappingInflater.InitNatMappingsPerCluster(subnets.RemotePodCIDR, externalCIDR, clusterID) } +// terminateNatMappingsPerCluster is used to update endpointMappings after a cluster peering is terminated. +func (liqoIPAM *IPAM) terminateNatMappingsPerCluster(clusterID string) error { + // Get NAT mappings + // natMappings keys are the set of endpoint reflected on remote cluster. + natMappings, err := liqoIPAM.natMappingInflater.GetNatMappings(clusterID) + if err != nil && !errors.Is(err, &liqoneterrors.MissingInit{ + StructureName: clusterID, + }) { + // Unknown error + return fmt.Errorf("cannot get NAT mappings for cluster %s:%w", clusterID, err) + } + if err != nil && errors.Is(err, &liqoneterrors.MissingInit{ + StructureName: clusterID, + }) { + /* + This can happen if: + a: terminateNatMappingsPerCluster has been called more than once after initialization. + b. terminateNatMappingsPerCluster has been called once without previous initialization. + In both circumstances, there are no actions to be performed here. + */ + return nil + } + + // Get endpointMappings + endpointMappings, err := liqoIPAM.ipamStorage.getEndpointMappings() + if err != nil { + return fmt.Errorf("cannot get Endpoint IPs: %w", err) + } + + // Remove cluster from the list of clusters the endpoint is reflected in. + for ip := range natMappings { + m := endpointMappings[ip] + delete(m.ClusterMappings, clusterID) + if len(m.ClusterMappings) == 0 { + // There are no more clusters using this endpoint IP + + // Get local ExternalCIDR + localExternalCIDR, err := liqoIPAM.ipamStorage.getExternalCIDR() + if err != nil { + return fmt.Errorf("cannot get ExternalCIDR: %w", err) + } + // Free IP + if err := liqoIPAM.ipam.ReleaseIPFromPrefix(localExternalCIDR, endpointMappings[ip].IP); err != nil { + return fmt.Errorf("cannot free IP: %w", err) + } + klog.Infof("IP %s (mapped from %s) has been freed", endpointMappings[ip].IP, ip) + // Delete entry + delete(endpointMappings, ip) + } else { + endpointMappings[ip] = m + } + } + + // Update endpointMappings + if err := liqoIPAM.ipamStorage.updateEndpointMappings(endpointMappings); err != nil { + return fmt.Errorf("cannot update endpointMappings:%w", err) + } + + // Free/Remove resouces in Inflater + if err := liqoIPAM.natMappingInflater.TerminateNatMappingsPerCluster(clusterID); err != nil { + return err + } + return nil +} + // AddNetworkPool adds a network to the set of network pools. func (liqoIPAM *IPAM) AddNetworkPool(network string) error { // Get resource @@ -683,21 +759,40 @@ func (liqoIPAM *IPAM) RemoveNetworkPool(network string) error { // AddLocalSubnetsPerCluster stores how the PodCIDR and the ExternalCIDR of local cluster // has been remapped in a remote cluster. If no remapping happened, then the CIDR value should be equal to "None". func (liqoIPAM *IPAM) AddLocalSubnetsPerCluster(podCIDR, externalCIDR, clusterID string) error { - var exists bool + var subnetsExist, natMappingsPerClusterConfigured bool var subnets netv1alpha1.Subnets + if clusterID == "" { + return &liqoneterrors.WrongParameter{ + Parameter: consts.ClusterIDLabelName, + Reason: liqoneterrors.StringNotEmpty, + } + } + // Get cluster subnets clusterSubnets, err := liqoIPAM.ipamStorage.getClusterSubnets() if err != nil { return fmt.Errorf("cannot get cluster subnets: %w", err) } - // Check existence - subnets, exists = clusterSubnets[clusterID] - if exists && subnets.LocalNATPodCIDR != "" && subnets.LocalNATExternalCIDR != "" { + + // Get NatMappingsConfigured map + natMappingsConfigured, err := liqoIPAM.ipamStorage.getNatMappingsConfigured() + if err != nil { + return fmt.Errorf("cannot establish if NatMappings for cluster %s have been configured: %w", clusterID, err) + } + + // Check existence of subnets struct and NatMappings have already been configured + subnets, subnetsExist = clusterSubnets[clusterID] + _, natMappingsPerClusterConfigured = natMappingsConfigured[clusterID] + if !subnetsExist { + return fmt.Errorf("remote subnets for cluster %s do not exist yet. Call first GetSubnetsPerCluster", + clusterID) + } + if subnets.LocalNATPodCIDR != "" && subnets.LocalNATExternalCIDR != "" && natMappingsPerClusterConfigured { return nil } // Set networks - if exists { + if subnetsExist { subnets.LocalNATPodCIDR = podCIDR subnets.LocalNATExternalCIDR = externalCIDR } else { @@ -718,9 +813,15 @@ func (liqoIPAM *IPAM) AddLocalSubnetsPerCluster(podCIDR, externalCIDR, clusterID } // Init NAT mappings - if err := liqoIPAM.initNatMappingsPerCluster(clusterID); err != nil { + if err := liqoIPAM.initNatMappingsPerCluster(clusterID, subnets); err != nil { return fmt.Errorf("unable to initialize NAT mappings per cluster %s: %w", clusterID, err) } + + // Update natMappingsConfigured + natMappingsConfigured[clusterID] = netv1alpha1.ConfiguredCluster{} + if err := liqoIPAM.ipamStorage.updateNatMappingsConfigured(natMappingsConfigured); err != nil { + return fmt.Errorf("unable to update NatMappingsConfigured: %w", err) + } return nil } @@ -813,6 +914,11 @@ func (liqoIPAM *IPAM) mapIPToExternalCIDR(clusterID, remoteExternalCIDR, ip stri return "", fmt.Errorf("cannot map endpoint IP %s to ExternalCIDR:%w", endpointMapping.IP, err) } + // Add NAT mapping + if err := liqoIPAM.natMappingInflater.AddMapping(ip, newIP, clusterID); err != nil { + return "", fmt.Errorf("cannot add NAT mapping: %w", err) + } + return newIP, nil } diff --git a/pkg/liqonet/ipamStorage.go b/pkg/liqonet/ipamStorage.go index ff1a7e703c..e3ce06059b 100644 --- a/pkg/liqonet/ipamStorage.go +++ b/pkg/liqonet/ipamStorage.go @@ -20,14 +20,15 @@ import ( ) const ( - ipamNamePrefix = "ipamstorage-" - clusterSubnetUpdate = "clusterSubnets" - poolsUpdate = "pools" - prefixesUpdate = "prefixes" - externalCIDRUpdate = "externalCIDR" - endpointMappingsUpdate = "endpointMappings" - podCIDRUpdate = "podCIDR" - serviceCIDRUpdate = "serviceCIDR" + ipamNamePrefix = "ipamstorage-" + clusterSubnetUpdate = "clusterSubnets" + poolsUpdate = "pools" + prefixesUpdate = "prefixes" + externalCIDRUpdate = "externalCIDR" + endpointMappingsUpdate = "endpointMappings" + podCIDRUpdate = "podCIDR" + serviceCIDRUpdate = "serviceCIDR" + natMappingsConfiguredUpdate = "natMappingsConfigured" ) // IpamStorage is the interface to be implemented to enforce persistency in IPAM. @@ -38,12 +39,14 @@ type IpamStorage interface { updateEndpointMappings(endpoints map[string]netv1alpha1.EndpointMapping) error updatePodCIDR(podCIDR string) error updateServiceCIDR(serviceCIDR string) error + updateNatMappingsConfigured(natMappingsConfigured map[string]netv1alpha1.ConfiguredCluster) error getClusterSubnets() (map[string]netv1alpha1.Subnets, error) getPools() ([]string, error) getExternalCIDR() (string, error) getEndpointMappings() (map[string]netv1alpha1.EndpointMapping, error) getPodCIDR() (string, error) getServiceCIDR() (string, error) + getNatMappingsConfigured() (map[string]netv1alpha1.ConfiguredCluster, error) goipam.Storage } @@ -76,10 +79,11 @@ func NewIPAMStorage(dynClient dynamic.Interface) (*IPAMStorage, error) { Labels: map[string]string{consts.IpamStorageResourceLabelKey: consts.IpamStorageResourceLabelValue}, }, Spec: netv1alpha1.IpamSpec{ - Prefixes: make(map[string][]byte), - Pools: make([]string, 0), - ClusterSubnets: make(map[string]netv1alpha1.Subnets), - EndpointMappings: make(map[string]netv1alpha1.EndpointMapping), + Prefixes: make(map[string][]byte), + Pools: make([]string, 0), + ClusterSubnets: make(map[string]netv1alpha1.Subnets), + EndpointMappings: make(map[string]netv1alpha1.EndpointMapping), + NatMappingsConfigured: make(map[string]netv1alpha1.ConfiguredCluster), }, } unstructuredIpam, err := runtime.DefaultUnstructuredConverter.ToUnstructured(ipam) @@ -244,6 +248,10 @@ func (ipamStorage *IPAMStorage) updateServiceCIDR(serviceCIDR string) error { return ipamStorage.updateConfig(serviceCIDRUpdate, serviceCIDR) } +func (ipamStorage *IPAMStorage) updateNatMappingsConfigured(natMappingsConfigured map[string]netv1alpha1.ConfiguredCluster) error { + return ipamStorage.updateConfig(natMappingsConfiguredUpdate, natMappingsConfigured) +} + func (ipamStorage *IPAMStorage) updateConfig(updateType string, data interface{}) error { jsonData, err := json.Marshal(data) @@ -316,6 +324,14 @@ func (ipamStorage *IPAMStorage) getServiceCIDR() (string, error) { return ipam.Spec.ServiceCIDR, nil } +func (ipamStorage *IPAMStorage) getNatMappingsConfigured() (map[string]netv1alpha1.ConfiguredCluster, error) { + ipam, err := ipamStorage.getConfig() + if err != nil { + return nil, err + } + return ipam.Spec.NatMappingsConfigured, nil +} + func (ipamStorage *IPAMStorage) getConfig() (*netv1alpha1.IpamStorage, error) { res := &netv1alpha1.IpamStorage{} list, err := ipamStorage.dynClient. diff --git a/pkg/liqonet/ipam_test.go b/pkg/liqonet/ipam_test.go index 75fa61ab5e..37839246c5 100644 --- a/pkg/liqonet/ipam_test.go +++ b/pkg/liqonet/ipam_test.go @@ -11,9 +11,9 @@ import ( . "github.com/onsi/gomega" k8serrors "k8s.io/apimachinery/pkg/api/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic/fake" liqonetapi "github.com/liqotech/liqo/apis/net/v1alpha1" @@ -25,11 +25,12 @@ import ( ) var ipam *liqonet.IPAM -var dynClient dynamic.Interface +var dynClient *fake.FakeDynamicClient const ( clusterID1 = "cluster1" clusterID2 = "cluster2" + clusterID3 = "cluster3" homePodCIDR = "10.0.0.0/24" localNATPodCIDR = "10.0.1.0/24" localNATExternalCIDR = "192.168.30.0/24" @@ -120,7 +121,7 @@ var _ = Describe("Ipam", func() { Expect(err).To(BeNil()) }) AfterEach(func() { - ipam.StopGRPCServer() + ipam.Terminate() }) Describe("AcquireReservedSubnet", func() { @@ -299,8 +300,13 @@ var _ = Describe("Ipam", func() { }) }) }) - Describe("RemoveClusterConfig", func() { + BeforeEach(func() { + err := ipam.SetPodCIDR(homePodCIDR) + Expect(err).To(BeNil()) + _, err = ipam.GetExternalCIDR(uint8(24)) + Expect(err).To(BeNil()) + }) Context("Remove config for a configured cluster", func() { It("Should successfully remove the configuration", func() { _, _, err := ipam.GetSubnetsPerCluster(remotePodCIDR, remoteExternalCIDR, clusterID1) @@ -318,6 +324,10 @@ var _ = Describe("Ipam", func() { // Check if network have been freed Expect(ipamStorage.Spec.Prefixes).ToNot(HaveKey(remotePodCIDR)) Expect(ipamStorage.Spec.Prefixes).ToNot(HaveKey(remoteExternalCIDR)) + + // Check if NatMapping resource has been deleted + _, err = getNatMappingResourcePerCluster(clusterID1) + Expect(k8serrors.IsNotFound(err)).To(BeTrue()) }) }) Context("Call for a non-configured cluster", func() { @@ -325,13 +335,20 @@ var _ = Describe("Ipam", func() { // Get config before call ipamStorage, err := getIpamStorageResource() Expect(err).To(BeNil()) + // In BeforeEach resources for clusterID1 and clusterID2 + // are created. So the following call should + // return a NotFound + _, err = getNatMappingResourcePerCluster(clusterID3) + Expect(k8serrors.IsNotFound(err)).To(BeTrue()) - err = ipam.RemoveClusterConfig(clusterID1) + err = ipam.RemoveClusterConfig(clusterID3) Expect(err).To(BeNil()) // Get config after call newIpamStorage, err := getIpamStorageResource() Expect(err).To(BeNil()) + _, err = getNatMappingResourcePerCluster(clusterID3) + Expect(k8serrors.IsNotFound(err)).To(BeTrue()) Expect(ipamStorage).To(Equal(newIpamStorage)) }) @@ -348,6 +365,8 @@ var _ = Describe("Ipam", func() { // Get config before second call ipamStorage, err := getIpamStorageResource() Expect(err).To(BeNil()) + _, err = getNatMappingResourcePerCluster(clusterID3) + Expect(k8serrors.IsNotFound(err)).To(BeTrue()) // Second call err = ipam.RemoveClusterConfig(clusterID1) @@ -356,6 +375,8 @@ var _ = Describe("Ipam", func() { // Get config after call newIpamStorage, err := getIpamStorageResource() Expect(err).To(BeNil()) + _, err = getNatMappingResourcePerCluster(clusterID3) + Expect(k8serrors.IsNotFound(err)).To(BeTrue()) Expect(ipamStorage).To(Equal(newIpamStorage)) }) @@ -366,6 +387,123 @@ var _ = Describe("Ipam", func() { Expect(err).To(MatchError(fmt.Sprintf("%s must be %s", consts.ClusterIDLabelName, liqoneterrors.StringNotEmpty))) }) }) + Context("Remove config when the cluster has an active mapping and"+ + "the endpoint is not reflected in any other cluster", func() { + It("should delete the endpoint mapping", func() { + _, _, err := ipam.GetSubnetsPerCluster(remotePodCIDR, remoteExternalCIDR, clusterID1) + Expect(err).To(BeNil()) + + // Remote cluster has not remapped local ExternalCIDR + err = ipam.AddLocalSubnetsPerCluster(consts.DefaultCIDRValue, consts.DefaultCIDRValue, clusterID1) + Expect(err).To(BeNil()) + + response, err := ipam.MapEndpointIP(context.Background(), + &liqonet.MapRequest{ + ClusterID: clusterID1, + Ip: externalEndpointIP, + }) + Expect(err).To(BeNil()) + // It should have mapped the IP + newIP := response.GetIp() + Expect(newIP).ToNot(Equal(externalEndpointIP)) + + // Add mapping to resource NatMapping + natMappingResource, err := getNatMappingResourcePerCluster(clusterID1) + Expect(err).To(BeNil()) + natMappingResource.Spec.ClusterMappings = map[string]string{ + externalEndpointIP: newIP, + } + err = updateNatMappingResource(natMappingResource) + Expect(err).To(BeNil()) + + // Terminate mappings with active mapping + err = ipam.RemoveClusterConfig(clusterID1) + Expect(err).To(BeNil()) + + // Check if resource exists + natMappingResource, err = getNatMappingResourcePerCluster(clusterID1) + Expect(k8serrors.IsNotFound(err)).To(BeTrue()) + + // Check if cluster has been deleted from cluster list of endpoint + ipamStorage, err := getIpamStorageResource() + + // Since the endpoint had only one mapping, the terminate should have deleted it. + Expect(ipamStorage.Spec.EndpointMappings).ToNot(HaveKey(externalEndpointIP)) + }) + }) + Context("Remove config when the cluster has an active mapping and"+ + "the endpoint is reflected in more clusters", func() { + It("should not remove the mapping", func() { + _, _, err := ipam.GetSubnetsPerCluster(remotePodCIDR, remoteExternalCIDR, clusterID1) + Expect(err).To(BeNil()) + _, _, err = ipam.GetSubnetsPerCluster(remotePodCIDR, remoteExternalCIDR, clusterID2) + Expect(err).To(BeNil()) + + err = ipam.AddLocalSubnetsPerCluster(consts.DefaultCIDRValue, consts.DefaultCIDRValue, clusterID1) + Expect(err).To(BeNil()) + err = ipam.AddLocalSubnetsPerCluster(consts.DefaultCIDRValue, consts.DefaultCIDRValue, clusterID2) + Expect(err).To(BeNil()) + + response, err := ipam.MapEndpointIP(context.Background(), + &liqonet.MapRequest{ + ClusterID: clusterID1, + Ip: externalEndpointIP, + }) + Expect(err).To(BeNil()) + // It should have mapped the IP + newIPInCluster1 := response.GetIp() + Expect(newIPInCluster1).ToNot(Equal(externalEndpointIP)) + + response, err = ipam.MapEndpointIP(context.Background(), + &liqonet.MapRequest{ + ClusterID: clusterID2, + Ip: externalEndpointIP, + }) + Expect(err).To(BeNil()) + // It should have mapped the IP + newIPInCluster2 := response.GetIp() + Expect(newIPInCluster2).ToNot(Equal(externalEndpointIP)) + + // Add mapping to resource NatMapping + natMappingResource, err := getNatMappingResourcePerCluster(clusterID1) + Expect(err).To(BeNil()) + natMappingResource.Spec.ClusterMappings = map[string]string{ + externalEndpointIP: newIPInCluster1, + } + err = updateNatMappingResource(natMappingResource) + Expect(err).To(BeNil()) + + // Cluster2 + natMappingResource, err = getNatMappingResourcePerCluster(clusterID2) + Expect(err).To(BeNil()) + natMappingResource.Spec.ClusterMappings = map[string]string{ + externalEndpointIP: newIPInCluster2, + } + err = updateNatMappingResource(natMappingResource) + Expect(err).To(BeNil()) + + // Terminate mappings with active mapping + err = ipam.RemoveClusterConfig(clusterID1) + Expect(err).To(BeNil()) + + // Check if resource exists + natMappingResource, err = getNatMappingResourcePerCluster(clusterID1) + Expect(k8serrors.IsNotFound(err)).To(BeTrue()) + + // Get IPAM configuration + ipamStorage, err := getIpamStorageResource() + Expect(err).To(BeNil()) + + // Since the endpoint had more than one mapping, the terminate should not have deleted it. + Expect(ipamStorage.Spec.EndpointMappings).To(HaveKey(externalEndpointIP)) + + // Get endpoint + endpointMapping := ipamStorage.Spec.EndpointMappings[externalEndpointIP] + // Check if cluster exists in clusterMappings + clusterMappings := endpointMapping.ClusterMappings + Expect(clusterMappings).ToNot(HaveKey(clusterID1)) + }) + }) }) Describe("FreeReservedSubnet", func() { @@ -407,7 +545,7 @@ var _ = Describe("Ipam", func() { Expect(e).To(Equal("10.0.2.0/24")) // Simulate re-scheduling - ipam.StopGRPCServer() + ipam.Terminate() ipam = liqonet.NewIPAM() err = ipam.Init(liqonet.Pools, dynClient, 2000+rand.Intn(2000)) Expect(err).To(BeNil()) @@ -549,32 +687,80 @@ var _ = Describe("Ipam", func() { }) Describe("AddLocalSubnetsPerCluster", func() { + var externalCIDR string BeforeEach(func() { // Set PodCIDR - err := ipam.SetPodCIDR(homePodCIDR) + err := ipam.SetPodCIDR("10.0.0.0/24") Expect(err).To(BeNil()) - // Get ExternalCIDR - externalCIDR, err := ipam.GetExternalCIDR(24) + // Set ExternalCIDR + externalCIDR, err = ipam.GetExternalCIDR(24) Expect(err).To(BeNil()) Expect(externalCIDR).To(HaveSuffix("/24")) - - // Assign networks to remote cluster - _, _, err = ipam.GetSubnetsPerCluster("10.0.0.0/24", "10.0.1.0/24", clusterID1) - Expect(err).To(BeNil()) }) - Context("If the networks do not exist yet", func() { - It("should return no errors", func() { - err := ipam.AddLocalSubnetsPerCluster("10.0.0.0/24", "192.168.0.0/24", clusterID1) + Context("Passing an empty clusterID", func() { + It("should return a WrongParameter error", func() { + _, _, err := ipam.GetSubnetsPerCluster(remotePodCIDR, remoteExternalCIDR, clusterID1) + Expect(err).To(BeNil()) + err = ipam.AddLocalSubnetsPerCluster(localNATPodCIDR, localNATExternalCIDR, "") + Expect(err).To(MatchError(fmt.Sprintf("%s must be %s", consts.ClusterIDLabelName, liqoneterrors.StringNotEmpty))) + }) + }) + Context("Call before GetSubnetsPerCluster", func() { + It("should return an error", func() { + err := ipam.AddLocalSubnetsPerCluster(localNATPodCIDR, localNATExternalCIDR, clusterID1) + Expect(err).To(MatchError(fmt.Sprintf("remote subnets for cluster %s do not exist yet. "+ + "Call first GetSubnetsPerCluster", clusterID1))) + }) + }) + Context("Call function", func() { + It("should update IpamStorage and create NatMappings resource", func() { + _, _, err := ipam.GetSubnetsPerCluster(remotePodCIDR, remoteExternalCIDR, clusterID1) + Expect(err).To(BeNil()) + err = ipam.AddLocalSubnetsPerCluster(localNATPodCIDR, localNATExternalCIDR, clusterID1) Expect(err).To(BeNil()) + + ipamStorage, err := getIpamStorageResource() + Expect(err).To(BeNil()) + + // Check IpamStorage + Expect(ipamStorage.Spec.ClusterSubnets).To(HaveKey(clusterID1)) + subnets := ipamStorage.Spec.ClusterSubnets[clusterID1] + Expect(subnets.LocalNATPodCIDR).To(Equal(localNATPodCIDR)) + Expect(subnets.LocalNATExternalCIDR).To(Equal(localNATExternalCIDR)) + + natMappings, err := getNatMappingResourcePerCluster(clusterID1) + Expect(err).To(BeNil()) + Expect(natMappings.Spec.ClusterID).To(Equal(clusterID1)) + Expect(natMappings.Spec.PodCIDR).To(Equal(remotePodCIDR)) + Expect(natMappings.Spec.ExternalCIDR).To(Equal(localNATExternalCIDR)) }) }) - Context("If the networks already exist", func() { - It("should return no errors", func() { - err := ipam.AddLocalSubnetsPerCluster("10.0.0.0/24", "192.168.0.0/24", clusterID1) + Context("Call func twice", func() { + It("second call should be a nop", func() { + _, _, err := ipam.GetSubnetsPerCluster(remotePodCIDR, remoteExternalCIDR, clusterID1) + Expect(err).To(BeNil()) + err = ipam.AddLocalSubnetsPerCluster(localNATPodCIDR, localNATExternalCIDR, clusterID1) + Expect(err).To(BeNil()) + + // Get config before second call + ipamStorage, err := getIpamStorageResource() + Expect(err).To(BeNil()) + natMappings, err := getNatMappingResourcePerCluster(clusterID1) + Expect(err).To(BeNil()) + + // Second call + err = ipam.AddLocalSubnetsPerCluster(localNATPodCIDR, localNATExternalCIDR, clusterID1) Expect(err).To(BeNil()) - err = ipam.AddLocalSubnetsPerCluster("10.0.0.0/24", "192.168.0.0/24", clusterID1) + + // Get config after second call + newIpamStorage, err := getIpamStorageResource() Expect(err).To(BeNil()) + newNatMappings, err := getNatMappingResourcePerCluster(clusterID1) + Expect(err).To(BeNil()) + + Expect(ipamStorage).To(Equal(newIpamStorage)) + Expect(natMappings).To(Equal(newNatMappings)) }) }) }) @@ -705,6 +891,13 @@ var _ = Describe("Ipam", func() { _, _, err = ipam.GetSubnetsPerCluster(remotePodCIDR, remoteExternalCIDR, clusterID1) Expect(err).To(BeNil()) + // Set ExternalCIDR + _, err = ipam.GetExternalCIDR(24) + Expect(err).To(BeNil()) + + _, _, err = ipam.GetSubnetsPerCluster("10.0.1.0/24", "10.0.2.0/24", "cluster1") + Expect(err).To(BeNil()) + // Remote cluster has not remapped local PodCIDR err = ipam.AddLocalSubnetsPerCluster(consts.DefaultCIDRValue, consts.DefaultCIDRValue, clusterID1) Expect(err).To(BeNil()) @@ -715,6 +908,10 @@ var _ = Describe("Ipam", func() { }) Expect(err).To(BeNil()) Expect(response.GetIp()).To(Equal("10.0.0.1")) + + // Should not create a mapping in NatMapping resource + nm, err := getNatMappingResourcePerCluster(clusterID1) + Expect(nm.Spec.ClusterMappings).ToNot(HaveKey("10.0.0.1")) }) }) Context("and the remote cluster has remapped the local PodCIDR", func() { @@ -732,6 +929,13 @@ var _ = Describe("Ipam", func() { _, _, err = ipam.GetSubnetsPerCluster(remotePodCIDR, remoteExternalCIDR, clusterID1) Expect(err).To(BeNil()) + // Set ExternalCIDR + _, err = ipam.GetExternalCIDR(24) + Expect(err).To(BeNil()) + + _, _, err = ipam.GetSubnetsPerCluster("10.0.1.0/24", "10.0.2.0/24", "cluster1") + Expect(err).To(BeNil()) + // Remote cluster has remapped local PodCIDR err = ipam.AddLocalSubnetsPerCluster("192.168.0.0/24", consts.DefaultCIDRValue, clusterID1) Expect(err).To(BeNil()) @@ -742,6 +946,10 @@ var _ = Describe("Ipam", func() { }) Expect(err).To(BeNil()) Expect(response.GetIp()).To(Equal("192.168.0.1")) + + // Should not create a mapping in NatMapping resource + nm, err := getNatMappingResourcePerCluster(clusterID1) + Expect(nm.Spec.ClusterMappings).ToNot(HaveKey("192.168.0.1")) }) }) }) @@ -773,6 +981,10 @@ var _ = Describe("Ipam", func() { slicedPrefix := strings.SplitN(externalCIDR, ".", 4) slicedPrefix = slicedPrefix[:len(slicedPrefix)-1] Expect(response.GetIp()).To(HavePrefix(strings.Join(slicedPrefix, "."))) + + // Should create a mapping in NatMapping resource + nm, err := getNatMappingResourcePerCluster(clusterID1) + Expect(nm.Spec.ClusterMappings).To(HaveKeyWithValue("20.0.0.1", response.GetIp())) }) It("should return the same IP if more remote clusters ask for the same endpoint", func() { // Set PodCIDR @@ -830,6 +1042,9 @@ var _ = Describe("Ipam", func() { _, _, err = ipam.GetSubnetsPerCluster(remotePodCIDR, remoteExternalCIDR, clusterID1) Expect(err).To(BeNil()) + _, _, err = ipam.GetSubnetsPerCluster("10.0.1.0/24", "10.0.2.0/24", "cluster1") + Expect(err).To(BeNil()) + // Remote cluster has remapped local ExternalCIDR err = ipam.AddLocalSubnetsPerCluster(consts.DefaultCIDRValue, "192.168.0.0/24", clusterID1) Expect(err).To(BeNil()) @@ -1135,6 +1350,31 @@ var _ = Describe("Ipam", func() { }) }) +func getNatMappingResourcePerCluster(clusterID string) (*liqonetapi.NatMapping, error) { + nm := &liqonetapi.NatMapping{} + list, err := dynClient.Resource(liqonetapi.NatMappingGroupResource).List( + context.Background(), + v1.ListOptions{ + LabelSelector: fmt.Sprintf("%s=%s,%s=%s", + consts.NatMappingResourceLabelKey, + consts.NatMappingResourceLabelValue, + consts.ClusterIDLabelName, + clusterID), + }, + ) + if err != nil { + return nil, err + } + if len(list.Items) == 0 { + return nil, k8serrors.NewNotFound(liqonetapi.NatMappingGroupResource.GroupResource(), "") + } + err = runtime.DefaultUnstructuredConverter.FromUnstructured(list.Items[0].Object, nm) + if err != nil { + return nil, err + } + return nm, nil +} + func getIpamStorageResource() (*liqonetapi.IpamStorage, error) { ipamConfig := &liqonetapi.IpamStorage{} list, err := dynClient.Resource(liqonetapi.IpamGroupResource).List( @@ -1157,3 +1397,19 @@ func getIpamStorageResource() (*liqonetapi.IpamStorage, error) { } return ipamConfig, nil } + +func updateNatMappingResource(natMapping *liqonetapi.NatMapping) error { + unstructuredResource, err := runtime.DefaultUnstructuredConverter.ToUnstructured(natMapping) + if err != nil { + return err + } + _, err = dynClient.Resource(liqonetapi.NatMappingGroupResource).Update( + context.Background(), + &unstructured.Unstructured{Object: unstructuredResource}, + v1.UpdateOptions{}, + ) + if err != nil { + return err + } + return nil +} diff --git a/pkg/liqonet/natmappinginflater/natMappingInflater.go b/pkg/liqonet/natmappinginflater/natMappingInflater.go index f01f4a263b..8b59cf1197 100644 --- a/pkg/liqonet/natmappinginflater/natMappingInflater.go +++ b/pkg/liqonet/natmappinginflater/natMappingInflater.go @@ -9,6 +9,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/dynamic" + "k8s.io/client-go/util/retry" "k8s.io/klog" netv1alpha1 "github.com/liqotech/liqo/apis/net/v1alpha1" @@ -26,8 +27,12 @@ type Interface interface { // externalCIDR is the ExternalCIDR used in the remote cluster for local exported resources: // it can be either the LocalExternalCIDR or the LocalNATExternalCIDR. InitNatMappingsPerCluster(podCIDR, externalCIDR, clusterID string) error + // TerminateNatMappingsPerCluster frees/deletes resources allocated for remote cluster. + TerminateNatMappingsPerCluster(clusterID string) error // GetNatMappings returns the set of mappings related to a remote cluster. GetNatMappings(clusterID string) (map[string]string, error) + // AddMapping adds a NAT mapping. + AddMapping(oldIP, newIP, clusterID string) error } // NatMappingInflater is an implementation of the NatMappingInflaterInterface @@ -165,6 +170,117 @@ func (inflater *NatMappingInflater) initResource(podCIDR, externalCIDR, clusterI return nil } +// TerminateNatMappingsPerCluster deletes the NatMapping resource for remote cluster. +func (inflater *NatMappingInflater) TerminateNatMappingsPerCluster(clusterID string) error { + if err := inflater.deleteResourceForCluster(clusterID); err != nil { + return fmt.Errorf("unable to delete resource for cluster %s: %w", clusterID, err) + } + // Remove entry in natMappingsPerCluster + delete(inflater.natMappingsPerCluster, clusterID) + return nil +} + +// Function that deletes the resource NatMapping for a specific remote cluster. +// It carries out multiple tentatives until it manages to delete the resource. +func (inflater *NatMappingInflater) deleteResourceForCluster(clusterID string) error { + retryError := retry.RetryOnConflict(retry.DefaultBackoff, func() error { + // Get resource for remote cluster + natMappings, err := inflater.getNatMappingResource(clusterID) + if err != nil && !k8sErr.IsNotFound(err) { + return fmt.Errorf("cannot retrieve NatMapping resource for cluster %s: %w", clusterID, err) + } + if err != nil && k8sErr.IsNotFound(err) { + return nil + } + // Remove labels before deleting resource is necessary + // because otherwise the informer will be triggered and will + // re-create the resource. + delete(natMappings.ObjectMeta.Labels, consts.NatMappingResourceLabelKey) + if err := inflater.updateNatMappingResource(natMappings); err != nil { + return fmt.Errorf("cannot update NatMapping resource for cluster %s: %w", clusterID, err) + } + // Delete resource + err = inflater.dynClient.Resource(netv1alpha1.NatMappingGroupResource).Delete( + context.Background(), natMappings.Name, metav1.DeleteOptions{}) + if err != nil { + return err + } + klog.Infof("NatMapping resource for cluster %s deleted", clusterID) + return nil + }) + if retryError != nil { + return retryError + } + return nil +} + +// AddMapping adds a mapping in the resource related to a remote cluster. +// It also adds the mapping in natMappingsPerCluster. +func (inflater *NatMappingInflater) AddMapping(oldIP, newIP, clusterID string) error { + var exists bool + var mappings netv1alpha1.Mappings + // Check if NAT mappings have been initilized for remote cluster. + mappings, exists = inflater.natMappingsPerCluster[clusterID] + if !exists { + return &errors.MissingInit{ + StructureName: fmt.Sprintf("%s for cluster %s", consts.NatMappingKind, clusterID), + } + } + // Check existence of mapping + existingIP, exists := mappings[oldIP] + if exists && existingIP == newIP { + return nil // Mapping already exists, do nothing + } + // Add/Update mapping in memory structure + mappings[oldIP] = newIP + if err := inflater.addOrUpdateMappingInResource(oldIP, newIP, clusterID); err != nil { + delete(mappings, oldIP) // Undo add + return fmt.Errorf("unable to add NatMapping to resource: %w", err) + } + return nil +} + +func (inflater *NatMappingInflater) addOrUpdateMappingInResource(oldIP, newIP, clusterID string) error { + retryError := retry.RetryOnConflict(retry.DefaultRetry, func() error { + // Get resource for remote cluster + natMappings, err := inflater.getNatMappingResource(clusterID) + if err != nil { + return fmt.Errorf("cannot retrieve NatMapping resource for cluster %s: %w", clusterID, err) + } + natMappings.Spec.ClusterMappings[oldIP] = newIP + // Update resource + if err := inflater.updateNatMappingResource(natMappings); err != nil { + return fmt.Errorf("cannot update NatMapping resource for cluster %s: %w", clusterID, err) + } + if err != nil { + return err + } + return nil + }) + if retryError != nil { + return retryError + } + return nil +} + +// Updates the resource related to a remote cluster. +func (inflater *NatMappingInflater) updateNatMappingResource(resource *netv1alpha1.NatMapping) error { + // Convert resource to unstructured type + unstructuredResource, err := runtime.DefaultUnstructuredConverter.ToUnstructured(resource) + if err != nil { + klog.Errorf("cannot map resource to unstructured resource: %s", err.Error()) + return err + } + + // Update + _, err = inflater.dynClient.Resource(netv1alpha1.NatMappingGroupResource).Update(context.Background(), + &unstructured.Unstructured{Object: unstructuredResource}, metav1.UpdateOptions{}) + if err != nil { + return err + } + return nil +} + // Retrieve resource relative to a remote cluster. func (inflater *NatMappingInflater) getNatMappingResource(clusterID string) (*netv1alpha1.NatMapping, error) { var res unstructured.Unstructured diff --git a/pkg/liqonet/natmappinginflater/natMappingInflater_test.go b/pkg/liqonet/natmappinginflater/natMappingInflater_test.go index 0d6490d20c..ce55f60574 100644 --- a/pkg/liqonet/natmappinginflater/natMappingInflater_test.go +++ b/pkg/liqonet/natmappinginflater/natMappingInflater_test.go @@ -35,6 +35,9 @@ const ( clusterID3 = "cluster-test-3" podCIDR = "10.0.0.0/24" externalCIDR = "10.0.1.0/24" + oldIP = "20.0.0.1" + newIP = "10.0.3.3" + newIP2 = "10.0.3.4" ) func setDynClient() error { @@ -279,4 +282,69 @@ var _ = Describe("NatMappingInflater", func() { }) }) }) + Describe("AddMapping", func() { + Context("Call func without initializing NAT mappings", func() { + It("should return a MissingInit error", func() { + err := inflater.AddMapping(oldIP, newIP, clusterID1) + Expect(err).To(MatchError(fmt.Sprintf("%s for cluster %s must be %s", consts.NatMappingKind, clusterID1, liqoneterrors.Initialization))) + }) + }) + Context("Call func after correct initialization", func() { + It("should successfully add the mapping", func() { + // Init + err := inflater.InitNatMappingsPerCluster(podCIDR, externalCIDR, clusterID1) + Expect(err).To(BeNil()) + + err = inflater.AddMapping(oldIP, newIP, clusterID1) + Expect(err).To(BeNil()) + mappings, err := inflater.GetNatMappings(clusterID1) + Expect(mappings).To(HaveKeyWithValue(oldIP, newIP)) + }) + }) + Context("Call func twice with same parameters", func() { + It("second call should be a nop", func() { + // Init + err := inflater.InitNatMappingsPerCluster(podCIDR, externalCIDR, clusterID1) + Expect(err).To(BeNil()) + + err = inflater.AddMapping(oldIP, newIP, clusterID1) + Expect(err).To(BeNil()) + + // Check config before second call + nm, err := inflater.getNatMappingResource(clusterID1) + Expect(err).To(BeNil()) + Expect(nm.Spec.ClusterMappings).To(HaveKeyWithValue(oldIP, newIP)) + + err = inflater.AddMapping(oldIP, newIP, clusterID1) + Expect(err).To(BeNil()) + + // Check config after + newNm, err := inflater.getNatMappingResource(clusterID1) + Expect(err).To(BeNil()) + Expect(newNm).To(Equal(nm)) + }) + }) + Context("Call func twice with different new IP", func() { + It("should return no errors and update the mapping", func() { + // Init + err := inflater.InitNatMappingsPerCluster(podCIDR, externalCIDR, clusterID1) + Expect(err).To(BeNil()) + + err = inflater.AddMapping(oldIP, newIP, clusterID1) + Expect(err).To(BeNil()) + + err = inflater.AddMapping(oldIP, newIP2, clusterID1) + Expect(err).To(BeNil()) + + // Check if inflater has been updated successfully + mappings, err := inflater.GetNatMappings(clusterID1) + Expect(mappings).To(HaveKeyWithValue(oldIP, newIP2)) + + // Check resource + nm, err := inflater.getNatMappingResource(clusterID1) + Expect(err).To(BeNil()) + Expect(nm.Spec.ClusterMappings).To(HaveKeyWithValue(oldIP, newIP2)) + }) + }) + }) })