diff --git a/api/v1alpha1/ionoscloudcluster_types.go b/api/v1alpha1/ionoscloudcluster_types.go index 69d087f..4f8f2f8 100644 --- a/api/v1alpha1/ionoscloudcluster_types.go +++ b/api/v1alpha1/ionoscloudcluster_types.go @@ -85,10 +85,10 @@ type IONOSCloudClusterSpec struct { Location Location `json:"location"` // +kubebuilder:validation:MinLength=1 - IdentityName string `json:"identityName"` - ControlPlaneEndpoint clusterv1.APIEndpoint `json:"controlPlaneEndpoint"` - Lans []IONOSLanSpec `json:"lans,omitempty"` - LoadBalancer *IONOSLoadBalancerSpec `json:"loadBalancer,omitempty"` + IdentityName string `json:"identityName"` + ControlPlaneEndpoint clusterv1.APIEndpoint `json:"controlPlaneEndpoint"` + Lans []IONOSLanSpec `json:"lans,omitempty"` + LoadBalancer IONOSLoadBalancerSpec `json:"loadBalancer,omitempty"` // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="DataCenterID is immutable" DataCenterID string `json:"dataCenterID,omitempty"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 88f6088..10030f1 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -192,11 +192,7 @@ func (in *IONOSCloudClusterSpec) DeepCopyInto(out *IONOSCloudClusterSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } - if in.LoadBalancer != nil { - in, out := &in.LoadBalancer, &out.LoadBalancer - *out = new(IONOSLoadBalancerSpec) - **out = **in - } + out.LoadBalancer = in.LoadBalancer if in.PublicLanID != nil { in, out := &in.PublicLanID, &out.PublicLanID *out = new(int32) diff --git a/internal/controller/ionoscloudcluster_controller.go b/internal/controller/ionoscloudcluster_controller.go index b4539fd..5893c12 100644 --- a/internal/controller/ionoscloudcluster_controller.go +++ b/internal/controller/ionoscloudcluster_controller.go @@ -202,9 +202,9 @@ func (r *IONOSCloudClusterReconciler) reconcileDataCenter(ctx *context.ClusterCo } func (r *IONOSCloudClusterReconciler) reconcileLan(ctx *context.ClusterContext) (*reconcile.Result, error) { - ctx.Logger.Info("Reconciling Lan") for i := range ctx.IONOSCloudCluster.Spec.Lans { lanSpec := &ctx.IONOSCloudCluster.Spec.Lans[i] + ctx.Logger.Info(fmt.Sprintf("Reconciling %s Lan", lanSpec.Name)) if lanSpec.LanID == nil { lanID, err := createLan(ctx, lanSpec.Public) if err != nil { @@ -218,11 +218,11 @@ func (r *IONOSCloudClusterReconciler) reconcileLan(ctx *context.ClusterContext) lan, resp, err := ctx.IONOSClient.GetLan(ctx, ctx.IONOSCloudCluster.Spec.DataCenterID, lanId) if err != nil && resp.StatusCode != http.StatusNotFound { - return &reconcile.Result{}, errors.Wrap(err, "error getting public Lan") + return &reconcile.Result{}, errors.Wrap(err, fmt.Sprintf("error getting %s Lan", lanSpec.Name)) } if resp.StatusCode == http.StatusNotFound || *lan.Metadata.State == STATE_BUSY { - return &reconcile.Result{RequeueAfter: defaultRetryIntervalOnBusy}, errors.New("public Lan not available (yet)") + return &reconcile.Result{RequeueAfter: defaultRetryIntervalOnBusy}, errors.New(fmt.Sprintf("%s Lan not available (yet)", lanSpec.Name)) } } @@ -233,10 +233,18 @@ func (r *IONOSCloudClusterReconciler) reconcileLan(ctx *context.ClusterContext) func (r *IONOSCloudClusterReconciler) reconcileLoadBalancer(ctx *context.ClusterContext) (*reconcile.Result, error) { ctx.Logger.Info("Reconciling LoadBalancer") lbSpec := ctx.IONOSCloudCluster.Spec.LoadBalancer + listenerLan := ctx.IONOSCloudCluster.Lan(lbSpec.ListenerLanRef.Name) + targetLan := ctx.IONOSCloudCluster.Lan(lbSpec.TargetLanRef.Name) + + if listenerLan == nil { + return &reconcile.Result{RequeueAfter: defaultRetryIntervalOnBusy}, errors.New(fmt.Sprintf("listener lb %s Lan not available (yet)", lbSpec.ListenerLanRef.Name)) + } + if targetLan == nil { + return &reconcile.Result{RequeueAfter: defaultRetryIntervalOnBusy}, errors.New(fmt.Sprintf("target lb %s Lan not available (yet)", lbSpec.TargetLanRef.Name)) + } + if lbSpec.ID == "" { loadBalancerName := fmt.Sprintf("lb-%s", ctx.Cluster.Name) - listenerLan := ctx.IONOSCloudCluster.Lan(lbSpec.ListenerLanRef.Name) - targetLan := ctx.IONOSCloudCluster.Lan(lbSpec.TargetLanRef.Name) loadBalancer := ionoscloud.NetworkLoadBalancer{ Entities: &ionoscloud.NetworkLoadBalancerEntities{ Forwardingrules: &ionoscloud.NetworkLoadBalancerForwardingRules{ diff --git a/internal/controller/ionoscloudcluster_controller_test.go b/internal/controller/ionoscloudcluster_controller_test.go index 505963f..e868968 100644 --- a/internal/controller/ionoscloudcluster_controller_test.go +++ b/internal/controller/ionoscloudcluster_controller_test.go @@ -86,6 +86,28 @@ var _ = Describe("IONOSCloudCluster controller", func() { Port: 6443, }, IdentityName: CAPICClusterIdentityName, + LoadBalancer: v1alpha1.IONOSLoadBalancerSpec{ + ListenerLanRef: v1alpha1.IONOSLanRefSpec{ + Name: "public", + }, + TargetLanRef: v1alpha1.IONOSLanRefSpec{ + Name: "private", + }, + }, + Lans: []v1alpha1.IONOSLanSpec{ + { + Name: "public", + Public: true, + }, + { + Name: "private", + Public: false, + }, + { + Name: "internet", + Public: true, + }, + }, }, } identitySecret = &v1.Secret{ diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index 2310429..f3f63e4 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -20,6 +20,10 @@ import ( goctx "context" b64 "encoding/base64" "fmt" + "net/http" + "strings" + "time" + v1alpha1 "github.com/GDATASoftwareAG/cluster-api-provider-ionoscloud/api/v1alpha1" "github.com/GDATASoftwareAG/cluster-api-provider-ionoscloud/internal/context" "github.com/GDATASoftwareAG/cluster-api-provider-ionoscloud/internal/utils" @@ -29,7 +33,6 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" apitypes "k8s.io/apimachinery/pkg/types" - "net/http" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1" clusterutilv1 "sigs.k8s.io/cluster-api/util" @@ -41,8 +44,6 @@ import ( ctrlutil "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/reconcile" - "strings" - "time" ) // IONOSCloudMachineReconciler reconciles a IONOSCloudMachine object @@ -183,48 +184,8 @@ func (r *IONOSCloudMachineReconciler) reconcileDelete(ctx *context.MachineContex } } - if ctx.IONOSCloudMachine.Spec.IP != nil { // todo - if rules, _, err := ctx.IONOSClient.GetLoadBalancerForwardingRules(ctx, ctx.IONOSCloudCluster.Spec.DataCenterID, ctx.IONOSCloudCluster.Spec.LoadBalancerID); err != nil { - return reconcile.Result{}, err - } else { - targetToDelete := ionoscloud.NetworkLoadBalancerForwardingRuleTarget{ - Ip: ctx.IONOSCloudMachine.Spec.IP, - Port: ionoscloud.PtrInt32(6443), - Weight: ionoscloud.PtrInt32(1), - } - for _, rule := range *rules.Items { - if *rule.Properties.ListenerPort != 6443 { - // we only care about the api server port - continue - } - - for _, target := range *rule.Properties.Targets { - if *target.Ip == *targetToDelete.Ip { - targets := *rule.Properties.Targets - targets = findAndDeleteByIP(targets, targetToDelete) - properties := ionoscloud.NetworkLoadBalancerForwardingRuleProperties{ - Algorithm: rule.Properties.Algorithm, - HealthCheck: rule.Properties.HealthCheck, - ListenerIp: rule.Properties.ListenerIp, - ListenerPort: rule.Properties.ListenerPort, - Name: rule.Properties.Name, - Protocol: rule.Properties.Protocol, - Targets: &targets, - } - if _, _, err = ctx.IONOSClient.PatchLoadBalancerForwardingRule( - ctx, - ctx.IONOSCloudCluster.Spec.DataCenterID, - ctx.IONOSCloudCluster.Spec.LoadBalancerID, - *rule.Id, - properties, - ); err != nil { - return reconcile.Result{}, err - } - - } - } - } - } + if err = r.reconcileDeleteLoadBalancerForwardingRule(ctx); err != nil { + return reconcile.Result{}, err } conditions.MarkFalse(ctx.IONOSCloudCluster, v1alpha1.ServerCreatedCondition, "ServerDeleted", clusterv1.ConditionSeverityInfo, "") @@ -234,6 +195,62 @@ func (r *IONOSCloudMachineReconciler) reconcileDelete(ctx *context.MachineContex return reconcile.Result{}, nil } +func (r *IONOSCloudMachineReconciler) reconcileDeleteLoadBalancerForwardingRule(ctx *context.MachineContext) error { + if !clusterutilv1.IsControlPlaneMachine(ctx.Machine) { + ctx.Logger.Info("Deleting IONOSCLoudMachine is not a control plane...no forwarding rule deletion required.") + return nil + } + + lbSpec := ctx.IONOSCloudCluster.Spec.LoadBalancer + nic := ctx.IONOSCloudMachine.NicByLan(lbSpec.TargetLanRef.Name) + + if nic == nil && nic.PrimaryIP != nil { + return nil + } + + if rules, _, err := ctx.IONOSClient.GetLoadBalancerForwardingRules(ctx, ctx.IONOSCloudCluster.Spec.DataCenterID, lbSpec.ID); err != nil { + return err + } else { + targetToDelete := ionoscloud.NetworkLoadBalancerForwardingRuleTarget{ + Ip: nic.PrimaryIP, + Port: ionoscloud.PtrInt32(6443), + Weight: ionoscloud.PtrInt32(1), + } + for _, rule := range *rules.Items { + if *rule.Properties.ListenerPort != 6443 { + // we only care about the api server port + continue + } + + for _, target := range *rule.Properties.Targets { + if *target.Ip == *targetToDelete.Ip { + targets := *rule.Properties.Targets + targets = findAndDeleteByIP(targets, targetToDelete) + properties := ionoscloud.NetworkLoadBalancerForwardingRuleProperties{ + Algorithm: rule.Properties.Algorithm, + HealthCheck: rule.Properties.HealthCheck, + ListenerIp: rule.Properties.ListenerIp, + ListenerPort: rule.Properties.ListenerPort, + Name: rule.Properties.Name, + Protocol: rule.Properties.Protocol, + Targets: &targets, + } + if _, _, err = ctx.IONOSClient.PatchLoadBalancerForwardingRule( + ctx, + ctx.IONOSCloudCluster.Spec.DataCenterID, + lbSpec.ID, + *rule.Id, + properties, + ); err != nil { + return err + } + } + } + } + } + return nil +} + func (r *IONOSCloudMachineReconciler) reconcileNormal(ctx *context.MachineContext) (reconcile.Result, error) { ctx.Logger.Info("Reconciling IONOSCloudCluster") @@ -352,7 +369,6 @@ func (r *IONOSCloudMachineReconciler) reconcileServer(ctx *context.MachineContex }, }, Properties: &ionoscloud.ServerProperties{ - Cores: ctx.IONOSCloudMachine.Spec.Cores, CpuFamily: ctx.IONOSCloudMachine.Spec.CpuFamily, Name: ionoscloud.ToPtr(ctx.IONOSCloudMachine.Name), @@ -377,13 +393,15 @@ func (r *IONOSCloudMachineReconciler) reconcileServer(ctx *context.MachineContex if resp.StatusCode == http.StatusNotFound || (server.Metadata != nil && *server.Metadata.State == STATE_BUSY) { return &reconcile.Result{RequeueAfter: defaultRetryIntervalOnBusy}, errors.New("server not yet created") } + ipObtained := false nics := *server.Entities.Nics.Items for _, nic := range nics { ips := *nic.Properties.Ips lan := ctx.IONOSCloudCluster.LanBy(nic.Properties.Lan) - if lan == nil { + if lan == nil || len(ips) != 0 { continue } + ipObtained = true ctx.IONOSCloudMachine.EnsureNic(v1alpha1.IONOSNicSpec{ LanRef: v1alpha1.IONOSLanRefSpec{ Name: lan.Name, @@ -392,7 +410,7 @@ func (r *IONOSCloudMachineReconciler) reconcileServer(ctx *context.MachineContex }) } - if ctx.IONOSCloudMachine.Spec.IP == nil { + if !ipObtained { return &reconcile.Result{RequeueAfter: defaultRetryIntervalOnBusy}, errors.New("server does not have an ip yet") } @@ -408,8 +426,8 @@ func (r *IONOSCloudMachineReconciler) reconcileLoadBalancerForwardingRule(ctx *c ctx.Logger.Info("Reconciled IONOSCLoudMachine is not a control plane...no forwarding rule needed.") return nil, nil } - lbSpec := ctx.IONOSCloudCluster.Spec.LoadBalancer + lbSpec := ctx.IONOSCloudCluster.Spec.LoadBalancer nic := ctx.IONOSCloudMachine.NicByLan(lbSpec.TargetLanRef.Name) if nic == nil && nic.PrimaryIP != nil { diff --git a/internal/controller/ionoscloudmachine_controller_test.go b/internal/controller/ionoscloudmachine_controller_test.go index 77fbd1a..53f5427 100644 --- a/internal/controller/ionoscloudmachine_controller_test.go +++ b/internal/controller/ionoscloudmachine_controller_test.go @@ -111,6 +111,28 @@ var _ = Describe("IONOSCloudMachine controller", func() { Port: 6443, }, IdentityName: CAPICClusterIdentityName, + LoadBalancer: v1alpha1.IONOSLoadBalancerSpec{ + ListenerLanRef: v1alpha1.IONOSLanRefSpec{ + Name: "public", + }, + TargetLanRef: v1alpha1.IONOSLanRefSpec{ + Name: "private", + }, + }, + Lans: []v1alpha1.IONOSLanSpec{ + { + Name: "public", + Public: true, + }, + { + Name: "private", + Public: false, + }, + { + Name: "internet", + Public: true, + }, + }, }, } capicMachine = &v1alpha1.IONOSCloudMachine{ @@ -139,6 +161,18 @@ var _ = Describe("IONOSCloudMachine controller", func() { }, IP: nil, ProviderID: "", + Nics: []v1alpha1.IONOSNicSpec{ + { + LanRef: v1alpha1.IONOSLanRefSpec{ + Name: "private", + }, + }, + { + LanRef: v1alpha1.IONOSLanRefSpec{ + Name: "internet", + }, + }, + }, }, } identitySecret = &v1.Secret{