From bb94b10c57795fd12e438a18b8a04a05792a22fa Mon Sep 17 00:00:00 2001 From: Tarun Chinmai Sekar <70169773+tchinmai7@users.noreply.github.com> Date: Mon, 7 Oct 2024 10:50:07 -0700 Subject: [PATCH] [feat] VLAN support for CAPL clusters (#525) Introduce VLAN support for CAPL clusters --------- Co-authored-by: Rahul Sharma --- api/v1alpha1/conversion.go | 5 ++ api/v1alpha1/zz_generated.conversion.go | 16 ++-- api/v1alpha2/linodecluster_types.go | 5 ++ api/v1alpha2/linodecluster_webhook.go | 7 ++ api/v1alpha2/linodecluster_webhook_test.go | 62 ++++++++++++++ ...cture.cluster.x-k8s.io_linodeclusters.yaml | 7 ++ ...uster.x-k8s.io_linodeclustertemplates.yaml | 7 ++ controller/linodecluster_controller.go | 2 + controller/linodecluster_controller_test.go | 17 ++++ .../linodemachine_controller_helpers.go | 80 ++++++++++++++----- templates/addons/ccm-linode/ccm-linode.yaml | 2 +- .../flavors/rke2/vlan/kustomization.yaml | 64 +++++++++++++++ util/vlanips.go | 79 ++++++++++++++++++ util/vlanips_test.go | 56 +++++++++++++ 14 files changed, 377 insertions(+), 32 deletions(-) create mode 100644 templates/flavors/rke2/vlan/kustomization.yaml create mode 100644 util/vlanips.go create mode 100644 util/vlanips_test.go diff --git a/api/v1alpha1/conversion.go b/api/v1alpha1/conversion.go index f05fbf6ab..50eafd006 100644 --- a/api/v1alpha1/conversion.go +++ b/api/v1alpha1/conversion.go @@ -91,3 +91,8 @@ func Convert_v1alpha2_LinodeObjectStorageBucket_To_v1alpha1_LinodeObjectStorageB } return autoConvert_v1alpha2_LinodeObjectStorageBucket_To_v1alpha1_LinodeObjectStorageBucket(in, out, scope) } + +func Convert_v1alpha2_LinodeClusterSpec_To_v1alpha1_LinodeClusterSpec(in *infrastructurev1alpha2.LinodeClusterSpec, out *LinodeClusterSpec, scope conversion.Scope) error { + // VLAN is not supported in v1alpha1 + return autoConvert_v1alpha2_LinodeClusterSpec_To_v1alpha1_LinodeClusterSpec(in, out, scope) +} diff --git a/api/v1alpha1/zz_generated.conversion.go b/api/v1alpha1/zz_generated.conversion.go index 3dd66629f..f1a63a23b 100644 --- a/api/v1alpha1/zz_generated.conversion.go +++ b/api/v1alpha1/zz_generated.conversion.go @@ -95,11 +95,6 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*v1alpha2.LinodeClusterSpec)(nil), (*LinodeClusterSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1alpha2_LinodeClusterSpec_To_v1alpha1_LinodeClusterSpec(a.(*v1alpha2.LinodeClusterSpec), b.(*LinodeClusterSpec), scope) - }); err != nil { - return err - } if err := s.AddGeneratedConversionFunc((*LinodeClusterStatus)(nil), (*v1alpha2.LinodeClusterStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha1_LinodeClusterStatus_To_v1alpha2_LinodeClusterStatus(a.(*LinodeClusterStatus), b.(*v1alpha2.LinodeClusterStatus), scope) }); err != nil { @@ -315,6 +310,11 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddConversionFunc((*v1alpha2.LinodeClusterSpec)(nil), (*LinodeClusterSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha2_LinodeClusterSpec_To_v1alpha1_LinodeClusterSpec(a.(*v1alpha2.LinodeClusterSpec), b.(*LinodeClusterSpec), scope) + }); err != nil { + return err + } if err := s.AddConversionFunc((*v1alpha2.LinodeMachineSpec)(nil), (*LinodeMachineSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha2_LinodeMachineSpec_To_v1alpha1_LinodeMachineSpec(a.(*v1alpha2.LinodeMachineSpec), b.(*LinodeMachineSpec), scope) }); err != nil { @@ -522,11 +522,6 @@ func autoConvert_v1alpha2_LinodeClusterSpec_To_v1alpha1_LinodeClusterSpec(in *v1 return nil } -// Convert_v1alpha2_LinodeClusterSpec_To_v1alpha1_LinodeClusterSpec is an autogenerated conversion function. -func Convert_v1alpha2_LinodeClusterSpec_To_v1alpha1_LinodeClusterSpec(in *v1alpha2.LinodeClusterSpec, out *LinodeClusterSpec, s conversion.Scope) error { - return autoConvert_v1alpha2_LinodeClusterSpec_To_v1alpha1_LinodeClusterSpec(in, out, s) -} - func autoConvert_v1alpha1_LinodeClusterStatus_To_v1alpha2_LinodeClusterStatus(in *LinodeClusterStatus, out *v1alpha2.LinodeClusterStatus, s conversion.Scope) error { out.Ready = in.Ready out.FailureReason = (*errors.ClusterStatusError)(unsafe.Pointer(in.FailureReason)) @@ -1171,6 +1166,7 @@ func autoConvert_v1alpha2_NetworkSpec_To_v1alpha1_NetworkSpec(in *v1alpha2.Netwo out.NodeBalancerID = (*int)(unsafe.Pointer(in.NodeBalancerID)) // WARNING: in.ApiserverNodeBalancerConfigID requires manual conversion: does not exist in peer-type // WARNING: in.AdditionalPorts requires manual conversion: does not exist in peer-type + // WARNING: in.UseVlan requires manual conversion: does not exist in peer-type return nil } diff --git a/api/v1alpha2/linodecluster_types.go b/api/v1alpha2/linodecluster_types.go index 87ed9f84d..0b64f77f7 100644 --- a/api/v1alpha2/linodecluster_types.go +++ b/api/v1alpha2/linodecluster_types.go @@ -148,6 +148,11 @@ type NetworkSpec struct { // additionalPorts contains list of ports to be configured with NodeBalancer. // +optional AdditionalPorts []LinodeNBPortConfig `json:"additionalPorts,omitempty"` + + // UseVlan provisions a cluster that uses VLANs instead of VPCs. IPAM is managed internally. + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" + // +optional + UseVlan bool `json:"useVlan,omitempty"` } type LinodeNBPortConfig struct { diff --git a/api/v1alpha2/linodecluster_webhook.go b/api/v1alpha2/linodecluster_webhook.go index c5888cc17..424d595ee 100644 --- a/api/v1alpha2/linodecluster_webhook.go +++ b/api/v1alpha2/linodecluster_webhook.go @@ -125,6 +125,13 @@ func (r *linodeClusterValidator) validateLinodeClusterSpec(ctx context.Context, } } + if spec.Network.UseVlan && spec.VPCRef != nil { + errs = append(errs, &field.Error{ + Field: "Cannot use VLANs and VPCs together. Unset `network.useVlan` or remove `vpcRef`", + Type: field.ErrorTypeInvalid, + }) + } + if len(errs) == 0 { return nil } diff --git a/api/v1alpha2/linodecluster_webhook_test.go b/api/v1alpha2/linodecluster_webhook_test.go index 0f907087f..45f36a144 100644 --- a/api/v1alpha2/linodecluster_webhook_test.go +++ b/api/v1alpha2/linodecluster_webhook_test.go @@ -225,3 +225,65 @@ func TestValidateDNSLinodeCluster(t *testing.T) { }), ) } + +func TestValidateVlanAndVPC(t *testing.T) { + t.Parallel() + + var ( + validCluster = LinodeCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example", + Namespace: "example", + }, + Spec: LinodeClusterSpec{ + Region: "us-ord", + Network: NetworkSpec{ + UseVlan: true, + }, + }, + } + inValidCluster = LinodeCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example", + Namespace: "example", + }, + Spec: LinodeClusterSpec{ + Region: "us-ord", + Network: NetworkSpec{ + UseVlan: true, + }, + VPCRef: &corev1.ObjectReference{ + Namespace: "example", + Name: "example", + Kind: "LinodeVPC", + }, + }, + } + validator = &linodeClusterValidator{} + ) + + NewSuite(t, mock.MockLinodeClient{}).Run( + OneOf( + Path( + Call("valid", func(ctx context.Context, mck Mock) { + mck.LinodeClient.EXPECT().GetRegion(gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes() + }), + Result("success", func(ctx context.Context, mck Mock) { + errs := validator.validateLinodeClusterSpec(ctx, mck.LinodeClient, validCluster.Spec) + require.Empty(t, errs) + }), + ), + ), + OneOf( + Path(Call("vlan and VPC set", func(ctx context.Context, mck Mock) { + mck.LinodeClient.EXPECT().GetRegion(gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes() + })), + ), + Result("error", func(ctx context.Context, mck Mock) { + errs := validator.validateLinodeClusterSpec(ctx, mck.LinodeClient, inValidCluster.Spec) + for _, err := range errs { + require.Contains(t, err.Error(), "Cannot use VLANs and VPCs together") + } + }), + ) +} diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_linodeclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_linodeclusters.yaml index 569c40ce1..160b746db 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_linodeclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_linodeclusters.yaml @@ -403,6 +403,13 @@ spec: nodeBalancerID: description: NodeBalancerID is the id of NodeBalancer. type: integer + useVlan: + description: UseVlan provisions a cluster that uses VLANs instead + of VPCs. IPAM is managed internally. + type: boolean + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf type: object region: description: The Linode Region the LinodeCluster lives in. diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_linodeclustertemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_linodeclustertemplates.yaml index 4aef1cbd1..179eb1471 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_linodeclustertemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_linodeclustertemplates.yaml @@ -331,6 +331,13 @@ spec: nodeBalancerID: description: NodeBalancerID is the id of NodeBalancer. type: integer + useVlan: + description: UseVlan provisions a cluster that uses VLANs + instead of VPCs. IPAM is managed internally. + type: boolean + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf type: object region: description: The Linode Region the LinodeCluster lives in. diff --git a/controller/linodecluster_controller.go b/controller/linodecluster_controller.go index 74a106d92..682087390 100644 --- a/controller/linodecluster_controller.go +++ b/controller/linodecluster_controller.go @@ -262,6 +262,8 @@ func (r *LinodeClusterReconciler) reconcileDelete(ctx context.Context, logger lo return errors.New("waiting for associated LinodeMachine objects to be deleted") } + util.DeleteClusterIPs(clusterScope.Cluster.Name, clusterScope.Cluster.Namespace) + if err := clusterScope.RemoveCredentialsRefFinalizer(ctx); err != nil { logger.Error(err, "failed to remove credentials finalizer") setFailureReason(clusterScope, cerrs.DeleteClusterError, err, r) diff --git a/controller/linodecluster_controller_test.go b/controller/linodecluster_controller_test.go index 95b772e80..fa1976eb6 100644 --- a/controller/linodecluster_controller_test.go +++ b/controller/linodecluster_controller_test.go @@ -382,6 +382,9 @@ var _ = Describe("cluster-delete", Ordered, Label("cluster", "cluster-delete"), cScope := &scope.ClusterScope{ LinodeCluster: &linodeCluster, + Cluster: &clusterv1.Cluster{ + ObjectMeta: metadata, + }, } ctlrSuite.BeforeEach(func(ctx context.Context, mck Mock) { @@ -390,8 +393,22 @@ var _ = Describe("cluster-delete", Ordered, Label("cluster", "cluster-delete"), ctlrSuite.Run( OneOf( + Path( + Call("cluster with vlan is deleted", func(ctx context.Context, mck Mock) { + cScope.LinodeCluster.Spec.Network.UseVlan = true + cScope.LinodeClient = mck.LinodeClient + cScope.Client = mck.K8sClient + mck.LinodeClient.EXPECT().DeleteNodeBalancer(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() + }), + Result("cluster with vlan deleted", func(ctx context.Context, mck Mock) { + reconciler.Client = mck.K8sClient + err := reconciler.reconcileDelete(ctx, logr.Logger{}, cScope) + Expect(err).NotTo(HaveOccurred()) + }), + ), Path( Call("cluster is deleted", func(ctx context.Context, mck Mock) { + cScope.LinodeCluster.Spec.Network.UseVlan = false cScope.LinodeClient = mck.LinodeClient cScope.Client = mck.K8sClient mck.LinodeClient.EXPECT().DeleteNodeBalancer(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() diff --git a/controller/linodemachine_controller_helpers.go b/controller/linodemachine_controller_helpers.go index 98be2b6aa..cbc7299c8 100644 --- a/controller/linodemachine_controller_helpers.go +++ b/controller/linodemachine_controller_helpers.go @@ -24,6 +24,7 @@ import ( "errors" "fmt" "net/http" + "net/netip" "slices" "sort" @@ -51,7 +52,10 @@ import ( // Size limit in bytes on the decoded metadata.user_data for cloud-init // The decoded user_data must not exceed 16384 bytes per the Linode API -const maxBootstrapDataBytes = 16384 +const ( + maxBootstrapDataBytes = 16384 + vlanIPFormat = "%s/11" +) var ( errNoPublicIPv4Addrs = errors.New("no public ipv4 addresses set") @@ -80,24 +84,7 @@ func retryIfTransient(err error) (ctrl.Result, error) { return ctrl.Result{}, err } -func newCreateConfig(ctx context.Context, machineScope *scope.MachineScope, logger logr.Logger) (*linodego.InstanceCreateOptions, error) { - var err error - - createConfig := linodeMachineSpecToInstanceCreateConfig(machineScope.LinodeMachine.Spec) - if createConfig == nil { - err = errors.New("failed to convert machine spec to create instance config") - - logger.Error(err, "Panic! Struct of LinodeMachineSpec is different than InstanceCreateOptions") - - return nil, err - } - - createConfig.Booted = util.Pointer(false) - - if err := setUserData(ctx, machineScope, createConfig, logger); err != nil { - return nil, err - } - +func fillCreateConfig(createConfig *linodego.InstanceCreateOptions, machineScope *scope.MachineScope) { if machineScope.LinodeMachine.Spec.PrivateIP != nil { createConfig.PrivateIP = *machineScope.LinodeMachine.Spec.PrivateIP } else { @@ -119,8 +106,28 @@ func newCreateConfig(ctx context.Context, machineScope *scope.MachineScope, logg if createConfig.RootPass == "" { createConfig.RootPass = uuid.NewString() } +} + +func newCreateConfig(ctx context.Context, machineScope *scope.MachineScope, logger logr.Logger) (*linodego.InstanceCreateOptions, error) { + var err error + + createConfig := linodeMachineSpecToInstanceCreateConfig(machineScope.LinodeMachine.Spec) + if createConfig == nil { + err = errors.New("failed to convert machine spec to create instance config") + + logger.Error(err, "Panic! Struct of LinodeMachineSpec is different than InstanceCreateOptions") + + return nil, err + } + + createConfig.Booted = util.Pointer(false) + if err := setUserData(ctx, machineScope, createConfig, logger); err != nil { + return nil, err + } + + fillCreateConfig(createConfig, machineScope) - // if vpc, attach additional interface as eth0 to linode + // if vpc is enabled, attach additional interface as eth0 to linode if machineScope.LinodeCluster.Spec.VPCRef != nil { iface, err := getVPCInterfaceConfig(ctx, machineScope, createConfig.Interfaces, logger) if err != nil { @@ -134,6 +141,15 @@ func newCreateConfig(ctx context.Context, machineScope *scope.MachineScope, logg } } + // if vlan is enabled, attach additional interface as eth0 to linode + if machineScope.LinodeCluster.Spec.Network.UseVlan { + iface := getVlanInterfaceConfig(machineScope, logger) + if iface != nil { + // add VLAN interface as first interface + createConfig.Interfaces = slices.Insert(createConfig.Interfaces, 0, *iface) + } + } + if machineScope.LinodeMachine.Spec.PlacementGroupRef != nil { pgID, err := getPlacementGroupID(ctx, machineScope, logger) if err != nil { @@ -191,7 +207,7 @@ func buildInstanceAddrs(ctx context.Context, machineScope *scope.MachineScope, i Type: clusterv1.MachineExternalIP, }) - // Iterate over interfaces in config and find VPC specific ips + // Iterate over interfaces in config and find VPC or VLAN specific ips for _, iface := range configs[0].Interfaces { if iface.VPCID != nil && iface.IPv4.VPC != "" { ips = append(ips, clusterv1.MachineAddress{ @@ -199,6 +215,14 @@ func buildInstanceAddrs(ctx context.Context, machineScope *scope.MachineScope, i Type: clusterv1.MachineInternalIP, }) } + + if iface.Purpose == linodego.InterfacePurposeVLAN { + // vlan addresses have a /11 appended to them - we should strip it out. + ips = append(ips, clusterv1.MachineAddress{ + Address: netip.MustParsePrefix(iface.IPAMAddress).Addr().String(), + Type: clusterv1.MachineInternalIP, + }) + } } // if a node has private ip, store it as well @@ -345,6 +369,20 @@ func getFirewallID(ctx context.Context, machineScope *scope.MachineScope, logger return *linodeFirewall.Spec.FirewallID, nil } +func getVlanInterfaceConfig(machineScope *scope.MachineScope, logger logr.Logger) *linodego.InstanceConfigInterfaceCreateOptions { + logger = logger.WithValues("vlanName", machineScope.Cluster.Name) + + // Try to obtain a IP for the machine using its name + ip := util.GetNextVlanIP(machineScope.Cluster.Name, machineScope.Cluster.Namespace) + logger.Info("obtained IP for machine", "name", machineScope.LinodeMachine.Name, "ip", ip) + + return &linodego.InstanceConfigInterfaceCreateOptions{ + Purpose: linodego.InterfacePurposeVLAN, + Label: machineScope.Cluster.Name, + IPAMAddress: fmt.Sprintf(vlanIPFormat, ip), + } +} + func getVPCInterfaceConfig(ctx context.Context, machineScope *scope.MachineScope, interfaces []linodego.InstanceConfigInterfaceCreateOptions, logger logr.Logger) (*linodego.InstanceConfigInterfaceCreateOptions, error) { name := machineScope.LinodeCluster.Spec.VPCRef.Name namespace := machineScope.LinodeCluster.Spec.VPCRef.Namespace diff --git a/templates/addons/ccm-linode/ccm-linode.yaml b/templates/addons/ccm-linode/ccm-linode.yaml index 6160c760b..e8065c3c5 100644 --- a/templates/addons/ccm-linode/ccm-linode.yaml +++ b/templates/addons/ccm-linode/ccm-linode.yaml @@ -9,7 +9,7 @@ spec: repoURL: https://linode.github.io/linode-cloud-controller-manager/ chartName: ccm-linode namespace: kube-system - version: ${LINODE_CCM_VERSION:=v0.4.14} + version: ${LINODE_CCM_VERSION:=v0.4.16} options: waitForJobs: true wait: true diff --git a/templates/flavors/rke2/vlan/kustomization.yaml b/templates/flavors/rke2/vlan/kustomization.yaml new file mode 100644 index 000000000..e501bcf91 --- /dev/null +++ b/templates/flavors/rke2/vlan/kustomization.yaml @@ -0,0 +1,64 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ../default + +patches: + - target: + kind: HelmChartProxy + name: .*-linode-cloud-controller-manager + patch: |- + - op: replace + path: /spec/valuesTemplate + value: | + secretRef: + name: "linode-token-region" + nodeSelector: + node-role.kubernetes.io/control-plane: "true" + - target: + kind: LinodeVPC + patch: |- + $patch: delete + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha2 + kind: LinodeVPC + metadata: + name: ${VPC_NAME:=${CLUSTER_NAME}} + - target: + group: bootstrap.cluster.x-k8s.io + version: v1beta1 + kind: RKE2ConfigTemplate + patch: |- + - op: replace + path: /spec/template/spec/preRKE2Commands + value: + - | + mkdir -p /etc/rancher/rke2/config.yaml.d/ + echo "node-ip: $(hostname -I | grep -oE ^10\.0\.[0-9]+\.[0-9]+)" >> /etc/rancher/rke2/config.yaml.d/capi-config.yaml + - sed -i '/swap/d' /etc/fstab + - swapoff -a + - hostnamectl set-hostname '{{ ds.meta_data.label }}' && hostname -F /etc/hostname + - target: + group: controlplane.cluster.x-k8s.io + version: v1beta1 + kind: RKE2ControlPlane + patch: |- + - op: replace + path: /spec/preRKE2Commands + value: + - | + mkdir -p /etc/rancher/rke2/config.yaml.d/ + echo "node-ip: $(hostname -I | grep -oE ^10\.0\.[0-9]+\.[0-9]+)" >> /etc/rancher/rke2/config.yaml.d/capi-config.yaml + - sed -i '/swap/d' /etc/fstab + - swapoff -a + - hostnamectl set-hostname '{{ ds.meta_data.label }}' && hostname -F /etc/hostname + - target: + group: infrastructure.cluster.x-k8s.io + version: v1alpha2 + kind: LinodeCluster + patch: |- + - op: remove + path: /spec/vpcRef + - op: add + path: /spec/network + value: + useVlan: true diff --git a/util/vlanips.go b/util/vlanips.go new file mode 100644 index 000000000..44a300c96 --- /dev/null +++ b/util/vlanips.go @@ -0,0 +1,79 @@ +/* +Copyright 2024 Akamai Technologies, Inc. + +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 util + +import ( + "fmt" + "net/netip" + "slices" + "sync" +) + +var ( + vlanIPsMu sync.RWMutex + // vlanIPsMap stores clusterName and a list of VlanIPs assigned to that cluster + vlanIPsMap = make(map[string]*ClusterIPs, 0) + vlanIPRange = "10.0.0.0/8" +) + +type ClusterIPs struct { + mu sync.RWMutex + ips []string +} + +func getClusterIPs(key string) *ClusterIPs { + vlanIPsMu.Lock() + defer vlanIPsMu.Unlock() + ips, exists := vlanIPsMap[key] + if !exists { + ips = &ClusterIPs{ + ips: []string{}, + } + } + return ips +} + +func (c *ClusterIPs) getNextIP() string { + c.mu.Lock() + defer c.mu.Unlock() + prefix := netip.MustParsePrefix(vlanIPRange) + currentIp := prefix.Addr().Next() + + ipString := currentIp.String() + for { + if !slices.Contains(c.ips, ipString) { + break + } + currentIp = currentIp.Next() + ipString = currentIp.String() + } + c.ips = append(c.ips, ipString) + return ipString +} + +// GetNextVlanIP returns the next available IP for a cluster +func GetNextVlanIP(clusterName, namespace string) string { + key := fmt.Sprintf("%s.%s", namespace, clusterName) + clusterIPs := getClusterIPs(key) + return clusterIPs.getNextIP() +} + +func DeleteClusterIPs(clusterName, namespace string) { + vlanIPsMu.Lock() + defer vlanIPsMu.Unlock() + delete(vlanIPsMap, fmt.Sprintf("%s.%s", namespace, clusterName)) +} diff --git a/util/vlanips_test.go b/util/vlanips_test.go new file mode 100644 index 000000000..b296edcb9 --- /dev/null +++ b/util/vlanips_test.go @@ -0,0 +1,56 @@ +/* +Copyright 2023 Akamai Technologies, Inc. + +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 util + +import ( + "reflect" + "testing" +) + +func TestGetNextVlanIP(t *testing.T) { + t.Parallel() + tests := []struct { + name string + clusterName string + clusterNamespace string + want string + }{ + { + name: "provide key which exists in map", + clusterName: "test", + clusterNamespace: "testna", + want: "10.0.0.3", + }, + { + name: "provide key which doesn't exist", + clusterName: "test", + clusterNamespace: "testnonexistent", + want: "10.0.0.1", + }, + } + for _, tt := range tests { + vlanIPsMap["testna.test"] = &ClusterIPs{ + ips: []string{"10.0.0.1", "10.0.0.2"}, + } + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if got := GetNextVlanIP(tt.clusterName, tt.clusterNamespace); !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetNextVlanIP() = %v, want %v", got, tt.want) + } + }) + } +}