From ebf3e384278fdaf57e7df2acff82dfcd517b7ee8 Mon Sep 17 00:00:00 2001 From: Jan Jansen Date: Mon, 20 Nov 2023 15:09:29 +0100 Subject: [PATCH] configure failover groups Signed-off-by: Jan Jansen --- api/v1alpha1/ionoscloudcluster_types.go | 9 ++- api/v1alpha1/zz_generated.deepcopy.go | 20 ++++++ ...e.cluster.x-k8s.io_ionoscloudclusters.yaml | 12 ++++ ...r.x-k8s.io_ionoscloudclustertemplates.yaml | 12 ++++ ...er.yaml => cluster_ionoscloudcluster.yaml} | 2 + ...noscloudmachinetemplate-control-plane.yaml | 2 +- ...pha1_ionoscloudmachinetemplate-worker.yaml | 2 +- config/samples/kubeadmcontrolplane.yaml | 2 +- config/samples/machinedeployment.yaml | 2 +- .../ionoscloudmachine_controller.go | 65 +++++++++++++++++++ internal/ionos/apiclient.go | 64 ++++++++++++++++++ testing/fakeClient.go | 14 ++++ 12 files changed, 201 insertions(+), 5 deletions(-) rename config/samples/{cluster_template_ionoscloudcluster.yaml => cluster_ionoscloudcluster.yaml} (86%) diff --git a/api/v1alpha1/ionoscloudcluster_types.go b/api/v1alpha1/ionoscloudcluster_types.go index 32c8cd4..f9e25e5 100644 --- a/api/v1alpha1/ionoscloudcluster_types.go +++ b/api/v1alpha1/ionoscloudcluster_types.go @@ -140,8 +140,15 @@ type IONOSLanSpec struct { LanID *int32 `json:"lanID,omitempty"` //validate? Name string `json:"name"` //validate? Public bool `json:"public"` + // +listType=map + // +listMapKey=id + FailoverGroups []IONOSFailoverGroup `json:"failoverGroups,omitempty"` //NameTemplate string `json:"nameTemplate"` - //FailoverIPs []string `json:"failoverIPs,omitempty"` +} + +type IONOSFailoverGroup struct { + ID string `json:"id"` + // NicUuid string `json:"nicUuid,omitempty"` } type IONOSLoadBalancerSpec struct { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 43efa55..6ca4fe7 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -536,6 +536,21 @@ func (in *IONOSCloudMachineTemplateSpec) DeepCopy() *IONOSCloudMachineTemplateSp return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IONOSFailoverGroup) DeepCopyInto(out *IONOSFailoverGroup) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IONOSFailoverGroup. +func (in *IONOSFailoverGroup) DeepCopy() *IONOSFailoverGroup { + if in == nil { + return nil + } + out := new(IONOSFailoverGroup) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IONOSLanRefSpec) DeepCopyInto(out *IONOSLanRefSpec) { *out = *in @@ -559,6 +574,11 @@ func (in *IONOSLanSpec) DeepCopyInto(out *IONOSLanSpec) { *out = new(int32) **out = **in } + if in.FailoverGroups != nil { + in, out := &in.FailoverGroups, &out.FailoverGroups + *out = make([]IONOSFailoverGroup, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IONOSLanSpec. diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml index 6cfb81c..9206537 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml @@ -59,6 +59,18 @@ spec: lans: items: properties: + failoverGroups: + items: + properties: + id: + type: string + required: + - id + type: object + type: array + x-kubernetes-list-map-keys: + - id + x-kubernetes-list-type: map lanID: format: int32 type: integer diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclustertemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclustertemplates.yaml index bb283c0..1017952 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclustertemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclustertemplates.yaml @@ -68,6 +68,18 @@ spec: lans: items: properties: + failoverGroups: + items: + properties: + id: + type: string + required: + - id + type: object + type: array + x-kubernetes-list-map-keys: + - id + x-kubernetes-list-type: map lanID: format: int32 type: integer diff --git a/config/samples/cluster_template_ionoscloudcluster.yaml b/config/samples/cluster_ionoscloudcluster.yaml similarity index 86% rename from config/samples/cluster_template_ionoscloudcluster.yaml rename to config/samples/cluster_ionoscloudcluster.yaml index d3dabf4..e235c99 100644 --- a/config/samples/cluster_template_ionoscloudcluster.yaml +++ b/config/samples/cluster_ionoscloudcluster.yaml @@ -15,6 +15,8 @@ spec: public: true - name: internet public: true + failoverGroups: + - id: 3682d039-e520-4837-810c-c43358949463 loadBalancer: listenerLanRef: name: public diff --git a/config/samples/infrastructure_v1alpha1_ionoscloudmachinetemplate-control-plane.yaml b/config/samples/infrastructure_v1alpha1_ionoscloudmachinetemplate-control-plane.yaml index 67c2781..4e18997 100644 --- a/config/samples/infrastructure_v1alpha1_ionoscloudmachinetemplate-control-plane.yaml +++ b/config/samples/infrastructure_v1alpha1_ionoscloudmachinetemplate-control-plane.yaml @@ -9,7 +9,7 @@ spec: ram: 4096 cpuFamily: "INTEL_SKYLAKE" bootVolume: - image: "a9152e16-2482-11ee-9eb0-82cf6aa9b8c3" + image: "3c2a7c7a-8488-11ee-811f-826459b45e91" type: "HDD" size: "25" nics: diff --git a/config/samples/infrastructure_v1alpha1_ionoscloudmachinetemplate-worker.yaml b/config/samples/infrastructure_v1alpha1_ionoscloudmachinetemplate-worker.yaml index 3de0c52..e5b1694 100644 --- a/config/samples/infrastructure_v1alpha1_ionoscloudmachinetemplate-worker.yaml +++ b/config/samples/infrastructure_v1alpha1_ionoscloudmachinetemplate-worker.yaml @@ -9,7 +9,7 @@ spec: ram: 2048 cpuFamily: "INTEL_SKYLAKE" bootVolume: - image: "a9152e16-2482-11ee-9eb0-82cf6aa9b8c3" + image: "3c2a7c7a-8488-11ee-811f-826459b45e91" type: "HDD" size: "25" nics: diff --git a/config/samples/kubeadmcontrolplane.yaml b/config/samples/kubeadmcontrolplane.yaml index efa80df..13498b2 100644 --- a/config/samples/kubeadmcontrolplane.yaml +++ b/config/samples/kubeadmcontrolplane.yaml @@ -47,4 +47,4 @@ spec: kind: IONOSCloudMachineTemplate name: ionoscloudcluster-control-plane replicas: 3 - version: "1.25.11" + version: "1.27.6" diff --git a/config/samples/machinedeployment.yaml b/config/samples/machinedeployment.yaml index e760583..c82dcdc 100644 --- a/config/samples/machinedeployment.yaml +++ b/config/samples/machinedeployment.yaml @@ -25,4 +25,4 @@ spec: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 kind: IONOSCloudMachineTemplate name: ionoscloudcluster-worker - version: "1.25.11" + version: "1.27.6" diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index 9144480..2ed09c1 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -414,11 +414,65 @@ func (r *IONOSCloudMachineReconciler) reconcileServer(ctx *context.MachineContex return &reconcile.Result{RequeueAfter: defaultRetryIntervalOnBusy}, errors.New("server does not have an ip yet") } + err = r.reconcileFailoverGroups(ctx, server) + if err != nil { + return &reconcile.Result{RequeueAfter: defaultRetryIntervalOnBusy}, err + } + conditions.MarkTrue(ctx.IONOSCloudMachine, v1alpha1.ServerCreatedCondition) return nil, nil } +func (r *IONOSCloudMachineReconciler) reconcileFailoverGroups(ctx *context.MachineContext, server ionoscloud.Server) error { + for i := range ctx.IONOSCloudCluster.Spec.Lans { + lanSpec := &ctx.IONOSCloudCluster.Spec.Lans[i] + serverNic := serverNicByLan(server, lanSpec) + if serverNic == nil { + continue + } + for k := range lanSpec.FailoverGroups { + group := &lanSpec.FailoverGroups[k] + ctx.Logger.Info("Reconciling failover group " + group.ID) + + block, _, err := ctx.IONOSClient.GetIPBlock(ctx, group.ID) + if err != nil { + return err + } + for _, ip := range *block.Properties.Ips { + err = ctx.IONOSClient.EnsureAdditionalIPOnNic(ctx, ctx.IONOSCloudCluster.Spec.DataCenterID, ctx.IONOSCloudMachine.Spec.ProviderID, *serverNic.Id, ip) + if err != nil { + return err + } + lanId := fmt.Sprint(*lanSpec.LanID) + lan, _, err := ctx.IONOSClient.GetLan(ctx, ctx.IONOSCloudCluster.Spec.DataCenterID, lanId) + if err != nil { + return err + } + registered := false + if lan.Properties.IpFailover != nil { + for _, f := range *lan.Properties.IpFailover { + if *f.Ip == ip { + registered = true + continue + } + } + if registered { + continue + } + } + + //todo only once per cluster and change if machine gets delete + err = ctx.IONOSClient.EnsureFailoverIPOnLan(ctx, ctx.IONOSCloudCluster.Spec.DataCenterID, lanId, ip, *serverNic.Id) + if err != nil { + return err + } + } + } + } + return nil +} + func (r *IONOSCloudMachineReconciler) reconcileLoadBalancerForwardingRule(ctx *context.MachineContext) (*reconcile.Result, error) { ctx.Logger.Info("Reconciling load balancer forwarding rule") @@ -494,6 +548,17 @@ func (r *IONOSCloudMachineReconciler) SetupWithManager(mgr ctrl.Manager) error { Complete(r) } +func serverNicByLan(server ionoscloud.Server, lan *v1alpha1.IONOSLanSpec) *ionoscloud.Nic { + var serverNic *ionoscloud.Nic + for _, nic := range *server.Entities.Nics.Items { + if *nic.Properties.Lan == *lan.LanID { + serverNic = &nic + break + } + } + return serverNic +} + func findAndDeleteByIP(s []ionoscloud.NetworkLoadBalancerForwardingRuleTarget, item ionoscloud.NetworkLoadBalancerForwardingRuleTarget) []ionoscloud.NetworkLoadBalancerForwardingRuleTarget { index := 0 for _, i := range s { diff --git a/internal/ionos/apiclient.go b/internal/ionos/apiclient.go index 28df8de..99f1a2d 100644 --- a/internal/ionos/apiclient.go +++ b/internal/ionos/apiclient.go @@ -22,9 +22,14 @@ type DatacenterAPI interface { DeleteDatacenter(ctx context.Context, datacenterId string) (*ionoscloud.APIResponse, error) } +type IPBlockAPI interface { + GetIPBlock(ctx context.Context, id string) (ionoscloud.IpBlock, *ionoscloud.APIResponse, error) +} + type LanAPI interface { CreateLan(ctx context.Context, datacenterId string, public bool) (ionoscloud.LanPost, *ionoscloud.APIResponse, error) GetLan(ctx context.Context, datacenterId, lanId string) (ionoscloud.Lan, *ionoscloud.APIResponse, error) + EnsureFailoverIPOnLan(ctx context.Context, datacenterId, lanId, ip, nicUuid string) error } type DefaultAPI interface { @@ -46,6 +51,7 @@ type ServerAPI interface { CreateServer(ctx context.Context, datacenterId string, server ionoscloud.Server) (ionoscloud.Server, *ionoscloud.APIResponse, error) GetServer(ctx context.Context, datacenterId, serverId string) (ionoscloud.Server, *ionoscloud.APIResponse, error) DeleteServer(ctx context.Context, datacenterId, serverId string) (*ionoscloud.APIResponse, error) + EnsureAdditionalIPOnNic(ctx context.Context, datacenterId, serverId, nic, ip string) error } type Client interface { @@ -55,6 +61,7 @@ type Client interface { ServerAPI DefaultAPI VolumeAPI + IPBlockAPI } func NewAPIClient(username, password, token, host string) Client { @@ -78,6 +85,63 @@ type APIClient struct { client *ionoscloud.APIClient } +func (c *APIClient) EnsureAdditionalIPOnNic(ctx context.Context, datacenterId, serverId, nicUuid, ip string) error { + serverId = strings.TrimPrefix(serverId, "ionos://") + nic, _, err := c.client.NetworkInterfacesApi.DatacentersServersNicsFindById(ctx, datacenterId, serverId, nicUuid).Execute() + if err != nil { + return err + } + ips := []string{} + if nic.Properties.Ips != nil { + for _, current := range *nic.Properties.Ips { + if current == ip { + return nil + } + } + ips = *nic.Properties.Ips + } + ips = append(ips, ip) + _, _, err = c.client.NetworkInterfacesApi.DatacentersServersNicsPatch(ctx, datacenterId, serverId, nicUuid).Nic(ionoscloud.NicProperties{ + Ips: &ips, + }).Execute() + return err +} + +func (c *APIClient) EnsureFailoverIPOnLan(ctx context.Context, datacenterId, lanId, ip, nicUuid string) error { + lan, _, err := c.client.LANsApi.DatacentersLansFindById(ctx, datacenterId, lanId).Execute() + if err != nil { + return err + } + failovers := []ionoscloud.IPFailover{} + ignore := false + if lan.Properties.IpFailover != nil { + for _, failover := range *lan.Properties.IpFailover { + if *failover.Ip == ip { + if *failover.NicUuid == nicUuid { + return nil + } + failover.NicUuid = &nicUuid + ignore = true + } + failovers = append(failovers, failover) + } + } + if !ignore { + failovers = append(failovers, ionoscloud.IPFailover{ + Ip: &ip, + NicUuid: &nicUuid, + }) + } + _, _, err = c.client.LANsApi.DatacentersLansPatch(ctx, datacenterId, lanId).Lan(ionoscloud.LanProperties{ + IpFailover: &failovers, + }).Execute() + return err +} + +func (c *APIClient) GetIPBlock(ctx context.Context, id string) (ionoscloud.IpBlock, *ionoscloud.APIResponse, error) { + return c.client.IPBlocksApi.IpblocksFindById(ctx, id).Execute() +} + func (c *APIClient) DeleteVolume(ctx context.Context, datacenterId, volumeId string) (*ionoscloud.APIResponse, error) { return c.client.VolumesApi.DatacentersVolumesDelete(ctx, datacenterId, volumeId).Execute() } diff --git a/testing/fakeClient.go b/testing/fakeClient.go index fd481ae..fd3a8ca 100644 --- a/testing/fakeClient.go +++ b/testing/fakeClient.go @@ -26,6 +26,20 @@ type FakeClient struct { CredentialsAreValid bool } +func (f FakeClient) EnsureFailoverIPOnLan(ctx context.Context, datacenterId, lanId, ip, nicUuid string) error { + //TODO implement me + panic("implement me") +} + +func (f FakeClient) EnsureAdditionalIPOnNic(ctx context.Context, datacenterId, serverId, nic, ip string) error { + //TODO implement me + panic("implement me") +} + +func (f FakeClient) GetIPBlock(_ context.Context, _ string) (ionoscloud.IpBlock, *ionoscloud.APIResponse, error) { + return ionoscloud.IpBlock{}, nil, nil +} + func (f FakeClient) DeleteServer(_ context.Context, datacenterId, serverId string) (*ionoscloud.APIResponse, error) { serverId = strings.TrimPrefix(serverId, "ionos://") var items []ionoscloud.Server