From 1b820ba07757a0acf3899419226029793fb826d4 Mon Sep 17 00:00:00 2001 From: Gustavo Alves Date: Mon, 8 Jan 2024 17:15:32 +0100 Subject: [PATCH 1/5] Add LAN provisioning without tests --- .codespellignore | 2 + api/v1alpha1/ionoscloudcluster_types.go | 4 + api/v1alpha1/ionoscloudmachine_types.go | 4 - hack/boilerplate.go.txt | 2 +- .../ionoscloudmachine_controller.go | 306 +++++++++++++++++- internal/ionoscloud/client.go | 7 +- internal/ionoscloud/client/client.go | 49 ++- internal/ionoscloud/client/errors.go | 4 +- pkg/scope/machine.go | 86 +++++ 9 files changed, 440 insertions(+), 24 deletions(-) create mode 100644 pkg/scope/machine.go diff --git a/.codespellignore b/.codespellignore index ce223b56..202f388b 100644 --- a/.codespellignore +++ b/.codespellignore @@ -1,3 +1,5 @@ capi capic decorder +reterr +ionos \ No newline at end of file diff --git a/api/v1alpha1/ionoscloudcluster_types.go b/api/v1alpha1/ionoscloudcluster_types.go index 559ccdcc..fe3b341c 100644 --- a/api/v1alpha1/ionoscloudcluster_types.go +++ b/api/v1alpha1/ionoscloudcluster_types.go @@ -54,8 +54,12 @@ type IonosCloudClusterStatus struct { // Conditions defines current service state of the IonosCloudCluster. // +optional Conditions clusterv1.Conditions `json:"conditions,omitempty"` + + PendingRequests PendingRequests `json:"PendingRequests,omitEmpty"` } +type PendingRequests map[string]*ProvisioningRequest + //+kubebuilder:object:root=true //+kubebuilder:subresource:status //+kubebuilder:printcolumn:name="Cluster",type="string",JSONPath=".metadata.labels['cluster\\.x-k8s\\.io/cluster-name']",description="Cluster" diff --git a/api/v1alpha1/ionoscloudmachine_types.go b/api/v1alpha1/ionoscloudmachine_types.go index a6e550b5..1d7ebc99 100644 --- a/api/v1alpha1/ionoscloudmachine_types.go +++ b/api/v1alpha1/ionoscloudmachine_types.go @@ -185,10 +185,6 @@ type IonosCloudMachineStatus struct { // Conditions defines current service state of the IonosCloudMachine. // +optional Conditions clusterv1.Conditions `json:"conditions,omitempty"` - - // CurrentRequest shows the current provisioning request for any - // cloud resource, that is being created. - CurrentRequest *ProvisioningRequest `json:"currentRequest,omitempty"` } //+kubebuilder:object:root=true diff --git a/hack/boilerplate.go.txt b/hack/boilerplate.go.txt index cc3f609d..7ecb9d36 100644 --- a/hack/boilerplate.go.txt +++ b/hack/boilerplate.go.txt @@ -1,5 +1,5 @@ /* -Copyright 2023 IONOS Cloud. +Copyright 2023-2024 IONOS Cloud. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index 30003e1b..942655b6 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -18,6 +18,15 @@ package controller import ( "context" + "errors" + "fmt" + "github.com/go-logr/logr" + "github.com/ionos-cloud/cluster-api-provider-ionoscloud/pkg/scope" + sdk "github.com/ionos-cloud/sdk-go/v6" + "k8s.io/klog/v2" + "k8s.io/utils/pointer" + "net/http" + "sigs.k8s.io/cluster-api/util/annotations" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" "sigs.k8s.io/cluster-api/util" @@ -52,10 +61,9 @@ type IonosCloudMachineReconciler struct { // // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.16.0/pkg/reconcile -func (r *IonosCloudMachineReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - _ = ctrl.LoggerFrom(ctx) +func (r *IonosCloudMachineReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) { + logger := ctrl.LoggerFrom(ctx) - // TODO(user): your logic here ionosCloudMachine := &infrav1.IonosCloudMachine{} if err := r.Client.Get(ctx, req.NamespacedName, ionosCloudMachine); err != nil { if apierrors.IsNotFound(err) { @@ -63,6 +71,93 @@ func (r *IonosCloudMachineReconciler) Reconcile(ctx context.Context, req ctrl.Re } return ctrl.Result{}, err } + + // Fetch the Machine. + machine, err := util.GetOwnerMachine(ctx, r.Client, ionosCloudMachine.ObjectMeta) + if err != nil { + return ctrl.Result{}, err + } + if machine == nil { + logger.Info("machine controller has not yet set OwnerRef") + return ctrl.Result{}, nil + } + + logger = logger.WithValues("machine", klog.KObj(machine)) + + // Fetch the Cluster. + cluster, err := util.GetClusterFromMetadata(ctx, r.Client, machine.ObjectMeta) + if err != nil { + logger.Info("machine is missing cluster label or cluster does not exist") + return ctrl.Result{}, nil + } + + if annotations.IsPaused(cluster, ionosCloudMachine) { + logger.Info("ionos cloud machine or linked cluster is marked as paused, not reconciling") + return ctrl.Result{}, nil + } + + logger = logger.WithValues("cluster", klog.KObj(cluster)) + + infraCluster, err := r.getInfraCluster(ctx, &logger, cluster, ionosCloudMachine) + if err != nil { + return ctrl.Result{}, fmt.Errorf("error getting infra provider cluster or control plane object: %w", err) + } + if infraCluster == nil { + logger.Info("ionos cloud machine is not ready yet") + return ctrl.Result{}, nil + } + + // Create the machine scope + machineScope, err := scope.NewMachineScope(scope.MachineScopeParams{ + Client: r.Client, + Cluster: cluster, + Machine: machine, + InfraCluster: infraCluster, + IonosCloudMachine: ionosCloudMachine, + Logger: &logger, + }) + if err != nil { + logger.Error(err, "failed to create scope") + return ctrl.Result{}, err + } + + //// Always close the scope when exiting this function, so we can persist any ProxmoxMachine changes. + //defer func() { + // if err := machineScope.Close(); err != nil && reterr == nil { + // reterr = err + // } + //}() + + if !ionosCloudMachine.ObjectMeta.DeletionTimestamp.IsZero() { + return r.reconcileDelete(ctx, machineScope) + } + + return r.reconcileNormal(ctx, machineScope) +} + +func (r *IonosCloudMachineReconciler) reconcileNormal( + ctx context.Context, machineScope *scope.MachineScope, +) (ctrl.Result, error) { + lan, err := r.reconcileLAN(ctx, machineScope) + if err != nil { + return ctrl.Result{}, fmt.Errorf("could not ensure lan: %w", err) + } + if lan == nil { + return ctrl.Result{Requeue: true}, nil + } + return ctrl.Result{}, nil +} + +func (r *IonosCloudMachineReconciler) reconcileDelete( + ctx context.Context, machineScope *scope.MachineScope, +) (ctrl.Result, error) { + shouldProceed, err := r.reconcileLANDelete(ctx, machineScope) + if err != nil { + return ctrl.Result{}, fmt.Errorf("could not ensure lan: %w", err) + } + if err == nil && !shouldProceed { + return ctrl.Result{Requeue: true}, nil + } return ctrl.Result{}, nil } @@ -75,3 +170,208 @@ func (r *IonosCloudMachineReconciler) SetupWithManager(mgr ctrl.Manager) error { handler.EnqueueRequestsFromMapFunc(util.MachineToInfrastructureMapFunc(infrav1.GroupVersion.WithKind(infrav1.IonosCloudMachineType)))). Complete(r) } + +func (r *IonosCloudMachineReconciler) getInfraCluster( + ctx context.Context, logger *logr.Logger, cluster *clusterv1.Cluster, ionosCloudMachine *infrav1.IonosCloudMachine, +) (*scope.ClusterScope, error) { + var clusterScope *scope.ClusterScope + var err error + + ionosCloudCluster := &infrav1.IonosCloudCluster{} + + infraClusterName := client.ObjectKey{ + Namespace: ionosCloudMachine.Namespace, + Name: cluster.Spec.InfrastructureRef.Name, + } + + if err := r.Client.Get(ctx, infraClusterName, ionosCloudCluster); err != nil { + // IonosCloudCluster is not ready + return nil, nil //nolint:nilerr + } + + // Create the cluster scope + clusterScope, err = scope.NewClusterScope(scope.ClusterScopeParams{ + Client: r.Client, + Logger: logger, + Cluster: cluster, + IonosCluster: ionosCloudCluster, + IonosClient: r.IonosCloudClient, + }) + if err != nil { + return nil, fmt.Errorf("failed to creat cluster scope: %w", err) + } + + return clusterScope, nil +} + +const lanFormatString = "%s-k8s-lan" + +func (r *IonosCloudMachineReconciler) reconcileLAN( + ctx context.Context, machineScope *scope.MachineScope, +) (*sdk.Lan, error) { + logger := machineScope.Logger + dataCenterID := machineScope.IonosCloudMachine.Spec.DatacenterID + ionos := r.IonosCloudClient + clusterScope := machineScope.ClusterScope + clusterName := clusterScope.Cluster.Name + var err error + var lan *sdk.Lan + + // try to find available LAN + lan, err = r.findLANWithinDatacenterLANs(ctx, machineScope) + if err != nil { + return nil, fmt.Errorf("could not search for LAN within LAN list: %w", err) + } + if lan == nil { + // check if there is a provisioning request + reqStatus, err := r.checkProvisioningRequest(ctx, machineScope) + if err != nil && reqStatus == "" { + return nil, fmt.Errorf("could not check status of provisioning request: %w", err) + } + if reqStatus != "" { + req := clusterScope.IonosCluster.Status.PendingRequests[dataCenterID] + l := logger.WithValues( + "requestURL", req.RequestPath, + "requestMethod", req.Method, + "requestStatus", req.State) + switch reqStatus { + case string(infrav1.RequestStatusFailed): + delete(clusterScope.IonosCluster.Status.PendingRequests, dataCenterID) + return nil, fmt.Errorf("provisioning request has failed: %w", err) + case string(infrav1.RequestStatusQueued), string(infrav1.RequestStatusRunning): + l.Info("provisioning request hasn't finished yet. trying again later.") + return nil, nil + case string(infrav1.RequestStatusDone): + lan, err = r.findLANWithinDatacenterLANs(ctx, machineScope) + if err != nil { + return nil, fmt.Errorf("could not search for lan within lan list: %w", err) + } + if lan == nil { + l.Info("pending provisioning request has finished, but lan could not be found. trying again later.") + return nil, nil + } + } + } + } else { + return lan, nil + } + // request LAN creation + requestURL, err := ionos.CreateLAN(ctx, dataCenterID, sdk.LanPropertiesPost{ + Name: pointer.String(fmt.Sprintf(lanFormatString, clusterName)), + Public: pointer.Bool(true), + }) + if err != nil { + return nil, fmt.Errorf("could not create a new LAN: %w ", err) + } + clusterScope.IonosCluster.Status.PendingRequests[dataCenterID] = &infrav1.ProvisioningRequest{Method: requestURL} + logger.WithValues("requestURL", requestURL).Info("new LAN creation was requested") + + return nil, nil +} + +func (r *IonosCloudMachineReconciler) findLANWithinDatacenterLANs( + ctx context.Context, machineScope *scope.MachineScope, +) (lan *sdk.Lan, err error) { + dataCenterID := machineScope.IonosCloudMachine.Spec.DatacenterID + ionos := r.IonosCloudClient + clusterScope := machineScope.ClusterScope + clusterName := clusterScope.Cluster.Name + + lans, err := ionos.ListLANs(ctx, dataCenterID) + if err != nil { + return nil, fmt.Errorf("could not list lans: %w", err) + } + if lans.Items != nil { + for _, lan := range *(lans.Items) { + if name := lan.Properties.Name; name != nil && *name == fmt.Sprintf(lanFormatString, clusterName) { + return &lan, nil + } + } + } + return nil, nil +} + +func (r *IonosCloudMachineReconciler) checkProvisioningRequest( + ctx context.Context, machineScope *scope.MachineScope, +) (string, error) { + clusterScope := machineScope.ClusterScope + ionos := r.IonosCloudClient + dataCenterID := machineScope.IonosCloudMachine.Spec.DatacenterID + request, requestExists := clusterScope.IonosCluster.Status.PendingRequests[dataCenterID] + + if requestExists { + reqStatus, err := ionos.CheckRequestStatus(ctx, request.RequestPath) + if err != nil { + return "", fmt.Errorf("could not check status of provisioning request: %w", err) + } + clusterScope.IonosCluster.Status.PendingRequests[dataCenterID].State = infrav1.RequestStatus(*reqStatus.Metadata.Status) + clusterScope.IonosCluster.Status.PendingRequests[dataCenterID].Message = *reqStatus.Metadata.Message + if *reqStatus.Metadata.Status != sdk.RequestStatusDone { + if metadata := *reqStatus.Metadata; *metadata.Status == sdk.RequestStatusFailed { + return sdk.RequestStatusFailed, errors.New(*metadata.Message) + } + return *reqStatus.Metadata.Status, nil + } + } + return "", nil +} + +func (r *IonosCloudMachineReconciler) reconcileLANDelete(ctx context.Context, machineScope *scope.MachineScope) (bool, error) { + logger := machineScope.Logger + clusterScope := machineScope.ClusterScope + dataCenterID := machineScope.IonosCloudMachine.Spec.DatacenterID + lan, err := r.findLANWithinDatacenterLANs(ctx, machineScope) + if err != nil { + return false, fmt.Errorf("error while trying to find lan: %w", err) + } + // Check if there is a provisioning request going on + if lan != nil { + reqStatus, err := r.checkProvisioningRequest(ctx, machineScope) + if err != nil && reqStatus == "" { + return false, fmt.Errorf("could not check status of provisioning request: %w", err) + } + if reqStatus != "" { + req := clusterScope.IonosCluster.Status.PendingRequests[dataCenterID] + l := logger.WithValues( + "requestURL", req.RequestPath, + "requestMethod", req.Method, + "requestStatus", req.State) + switch reqStatus { + case string(infrav1.RequestStatusFailed): + delete(clusterScope.IonosCluster.Status.PendingRequests, dataCenterID) + return false, fmt.Errorf("provisioning request has failed: %w", err) + case string(infrav1.RequestStatusQueued), string(infrav1.RequestStatusRunning): + l.Info("provisioning request hasn't finished yet. trying again later.") + return false, nil + case string(infrav1.RequestStatusDone): + lan, err = r.findLANWithinDatacenterLANs(ctx, machineScope) + if err != nil { + return false, fmt.Errorf("could not search for lan within lan list: %w", err) + } + if lan != nil { + l.Info("pending provisioning request has finished, but lan could still be found. trying again later.") + return false, nil + } + } + } + } + if lan == nil { + logger.Info("lan seems to be deleted.") + return true, nil + } + if lan.Entities.HasNics() { + logger.Info("lan seems like it is still being used. let whoever still uses it delete it.") + // NOTE: the LAN isn't deleted, but we can use the bool to signalize that we can proceed with the machine deletion. + return true, nil + } + requestURL, err := r.IonosCloudClient.DestroyLAN(ctx, dataCenterID, *lan.Id) + if err != nil { + return false, fmt.Errorf("could not destroy lan: %w", err) + } + machineScope.ClusterScope.IonosCluster.Status.PendingRequests[dataCenterID] = &infrav1.ProvisioningRequest{ + Method: http.MethodDelete, + RequestPath: requestURL, + } + logger.WithValues("requestURL", requestURL).Info("requested LAN deletion") + return false, nil +} diff --git a/internal/ionoscloud/client.go b/internal/ionoscloud/client.go index 7fb08f05..469861cd 100644 --- a/internal/ionoscloud/client.go +++ b/internal/ionoscloud/client.go @@ -40,8 +40,7 @@ type Client interface { // DestroyServer deletes the server that matches the provided serverID in the specified data center. DestroyServer(ctx context.Context, dataCenterID, serverID string) error // CreateLAN creates a new LAN with the provided properties in the specified data center. - CreateLAN(ctx context.Context, dataCenterID string, properties ionoscloud.LanPropertiesPost) ( - *ionoscloud.LanPost, error) + CreateLAN(ctx context.Context, dataCenterID string, properties ionoscloud.LanPropertiesPost) (string, error) // UpdateLAN updates a LAN with the provided properties in the specified data center. UpdateLAN(ctx context.Context, dataCenterID string, lanID string, properties ionoscloud.LanProperties) ( *ionoscloud.Lan, error) @@ -53,7 +52,9 @@ type Client interface { // GetLAN returns the LAN that matches lanID in the specified data center. GetLAN(ctx context.Context, dataCenterID, lanID string) (*ionoscloud.Lan, error) // DestroyLAN deletes the LAN that matches the provided lanID in the specified data center. - DestroyLAN(ctx context.Context, dataCenterID, lanID string) error + DestroyLAN(ctx context.Context, dataCenterID, lanID string) (string, error) + // CheckRequestStatus checks the status of a provided request identified by requestID + CheckRequestStatus(ctx context.Context, requestID string) (*ionoscloud.RequestStatus, error) // ListVolumes returns a list of volumes in a specified data center. ListVolumes(ctx context.Context, dataCenterID string) (*ionoscloud.Volumes, error) // GetVolume returns the volume that matches volumeID in the specified data center. diff --git a/internal/ionoscloud/client/client.go b/internal/ionoscloud/client/client.go index eab70d40..3844dbe8 100644 --- a/internal/ionoscloud/client/client.go +++ b/internal/ionoscloud/client/client.go @@ -143,20 +143,23 @@ func (c *IonosCloudClient) DestroyServer(ctx context.Context, dataCenterID, serv return err } -// CreateLAN creates a new LAN with the provided properties in the specified data center. +// CreateLAN creates a new LAN with the provided properties in the specified data center, returning the request ID. func (c *IonosCloudClient) CreateLAN(ctx context.Context, dataCenterID string, properties sdk.LanPropertiesPost, -) (*sdk.LanPost, error) { +) (string, error) { if dataCenterID == "" { - return nil, errDataCenterIDIsEmpty + return "", errDataCenterIDIsEmpty } lanPost := sdk.LanPost{ Properties: &properties, } - lp, _, err := c.API.LANsApi.DatacentersLansPost(ctx, dataCenterID).Lan(lanPost).Execute() + _, req, err := c.API.LANsApi.DatacentersLansPost(ctx, dataCenterID).Lan(lanPost).Execute() if err != nil { - return nil, fmt.Errorf(apiCallErrWrapper, err) + return "", fmt.Errorf(apiCallErrWrapper, err) + } + if location := req.Header.Get("Location"); location != "" { + return location, nil } - return &lp, nil + return "", errors.New(apiNoLocationErrWrapper) } // UpdateLAN updates a LAN with the provided properties in the specified data center. @@ -221,18 +224,18 @@ func (c *IonosCloudClient) GetLAN(ctx context.Context, dataCenterID, lanID strin } // DestroyLAN deletes the LAN that matches the provided lanID in the specified data center. -func (c *IonosCloudClient) DestroyLAN(ctx context.Context, dataCenterID, lanID string) error { +func (c *IonosCloudClient) DestroyLAN(ctx context.Context, dataCenterID, lanID string) (string, error) { if dataCenterID == "" { - return errDataCenterIDIsEmpty + return "", errDataCenterIDIsEmpty } if lanID == "" { - return errLanIDIsEmpty + return "", errLanIDIsEmpty } - _, err := c.API.LANsApi.DatacentersLansDelete(ctx, dataCenterID, lanID).Execute() + req, err := c.API.LANsApi.DatacentersLansDelete(ctx, dataCenterID, lanID).Execute() if err != nil { - return fmt.Errorf(apiCallErrWrapper, err) + return "", fmt.Errorf(apiCallErrWrapper, err) } - return nil + return req.Header.Get("Location"), nil } // ListVolumes returns a list of volumes in the specified data center. @@ -278,3 +281,25 @@ func (c *IonosCloudClient) DestroyVolume(ctx context.Context, dataCenterID, volu } return nil } + +func (c *IonosCloudClient) CheckRequestStatus(ctx context.Context, requestURL string) (*sdk.RequestStatus, error) { + if requestURL == "" { + return nil, errRequestURLIsEmpty + } + requestStatus, _, err := c.API.GetRequestStatus(ctx, requestURL) + if err != nil { + return nil, fmt.Errorf(apiCallErrWrapper, err) + } + return requestStatus, nil +} + +func (c *IonosCloudClient) WaitForRequest(ctx context.Context, requestURL string) error { + if requestURL == "" { + return errRequestURLIsEmpty + } + _, err := c.API.WaitForRequest(ctx, requestURL) + if err != nil { + return fmt.Errorf(apiCallErrWrapper, err) + } + return nil +} diff --git a/internal/ionoscloud/client/errors.go b/internal/ionoscloud/client/errors.go index 6f7baf72..a89335ad 100644 --- a/internal/ionoscloud/client/errors.go +++ b/internal/ionoscloud/client/errors.go @@ -23,8 +23,10 @@ var ( errServerIDIsEmpty = errors.New("error parsing server ID: value cannot be empty") errLanIDIsEmpty = errors.New("error parsing lan ID: value cannot be empty") errVolumeIDIsEmpty = errors.New("error parsing volume ID: value cannot be empty") + errRequestURLIsEmpty = errors.New("a request url is necessary for the operation") ) const ( - apiCallErrWrapper = "request to Cloud API has failed: %w" + apiCallErrWrapper = "request to Cloud API has failed: %w" + apiNoLocationErrWrapper = "request to Cloud API did not return the request url" ) diff --git a/pkg/scope/machine.go b/pkg/scope/machine.go new file mode 100644 index 00000000..9e13f550 --- /dev/null +++ b/pkg/scope/machine.go @@ -0,0 +1,86 @@ +/* + * Copyright 2024 IONOS Cloud. + * + * 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 scope + +import ( + "context" + "errors" + "fmt" + "github.com/go-logr/logr" + "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + "sigs.k8s.io/cluster-api/util/patch" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +type MachineScope struct { + *logr.Logger + + client client.Client + patchHelper *patch.Helper + Cluster *clusterv1.Cluster + Machine *clusterv1.Machine + + ClusterScope *ClusterScope + IonosCloudMachine *v1alpha1.IonosCloudMachine +} + +type MachineScopeParams struct { + Client client.Client + Logger *logr.Logger + Cluster *clusterv1.Cluster + Machine *clusterv1.Machine + InfraCluster *ClusterScope + IonosCloudMachine *v1alpha1.IonosCloudMachine +} + +func NewMachineScope(params MachineScopeParams) (*MachineScope, error) { + if params.Client == nil { + return nil, errors.New("machine scope params lack a client") + } + if params.Cluster == nil { + return nil, errors.New("machine scope params lack a cluster") + } + if params.Machine == nil { + return nil, errors.New("machine scope params lack a cluster api machine") + } + if params.IonosCloudMachine == nil { + return nil, errors.New("machine scope params lack a ionos cloud machine") + } + if params.InfraCluster == nil { + return nil, errors.New("machine scope params need a ionos cloud cluster scope") + } + if params.Logger == nil { + logger := log.FromContext(context.Background()) + params.Logger = &logger + } + helper, err := patch.NewHelper(params.IonosCloudMachine, params.Client) + if err != nil { + return nil, fmt.Errorf("failed to init patch helper: %w", err) + } + return &MachineScope{ + Logger: params.Logger, + client: params.Client, + patchHelper: helper, + Cluster: params.Cluster, + Machine: params.Machine, + ClusterScope: params.InfraCluster, + IonosCloudMachine: params.IonosCloudMachine, + }, nil +} From 58d73dd1387c3e6834fe4ea13ddf0698c7a186d9 Mon Sep 17 00:00:00 2001 From: Gustavo Alves Date: Mon, 8 Jan 2024 17:22:30 +0100 Subject: [PATCH 2/5] Update copyrights of some edited files --- api/v1alpha1/ionoscloudmachine_types_test.go | 16 ++++++++++++++++ .../controller/ionoscloudcluster_controller.go | 2 +- .../controller/ionoscloudmachine_controller.go | 2 +- internal/ionoscloud/client.go | 2 +- internal/ionoscloud/client/client.go | 2 +- internal/ionoscloud/client/client_test.go | 2 +- internal/ionoscloud/client/errors.go | 2 +- pkg/scope/machine.go | 2 +- 8 files changed, 23 insertions(+), 7 deletions(-) diff --git a/api/v1alpha1/ionoscloudmachine_types_test.go b/api/v1alpha1/ionoscloudmachine_types_test.go index 1a5c43e0..92aa9740 100644 --- a/api/v1alpha1/ionoscloudmachine_types_test.go +++ b/api/v1alpha1/ionoscloudmachine_types_test.go @@ -1,3 +1,19 @@ +/* +Copyright 2023-2024 IONOS Cloud. + +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 v1alpha1 import ( diff --git a/internal/controller/ionoscloudcluster_controller.go b/internal/controller/ionoscloudcluster_controller.go index f0029d2a..3eba04e4 100644 --- a/internal/controller/ionoscloudcluster_controller.go +++ b/internal/controller/ionoscloudcluster_controller.go @@ -1,5 +1,5 @@ /* -Copyright 2023 IONOS Cloud. +Copyright 2023-2024 IONOS Cloud. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index 942655b6..59a1879d 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -1,5 +1,5 @@ /* -Copyright 2023 IONOS Cloud. +Copyright 2023-2024 IONOS Cloud. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/internal/ionoscloud/client.go b/internal/ionoscloud/client.go index 469861cd..a97a925b 100644 --- a/internal/ionoscloud/client.go +++ b/internal/ionoscloud/client.go @@ -1,5 +1,5 @@ /* -Copyright 2023 IONOS Cloud. +Copyright 2023-2024 IONOS Cloud. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/internal/ionoscloud/client/client.go b/internal/ionoscloud/client/client.go index 3844dbe8..abd6f1ef 100644 --- a/internal/ionoscloud/client/client.go +++ b/internal/ionoscloud/client/client.go @@ -1,5 +1,5 @@ /* -Copyright 2023 IONOS Cloud. +Copyright 2023-2024 IONOS Cloud. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/internal/ionoscloud/client/client_test.go b/internal/ionoscloud/client/client_test.go index 802782ed..37dfe9e4 100644 --- a/internal/ionoscloud/client/client_test.go +++ b/internal/ionoscloud/client/client_test.go @@ -1,5 +1,5 @@ /* -Copyright 2023 IONOS Cloud. +Copyright 2023-2024 IONOS Cloud. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/internal/ionoscloud/client/errors.go b/internal/ionoscloud/client/errors.go index a89335ad..79964097 100644 --- a/internal/ionoscloud/client/errors.go +++ b/internal/ionoscloud/client/errors.go @@ -1,5 +1,5 @@ /* -Copyright 2023 IONOS Cloud. +Copyright 2023-2024 IONOS Cloud. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/pkg/scope/machine.go b/pkg/scope/machine.go index 9e13f550..2aafa33b 100644 --- a/pkg/scope/machine.go +++ b/pkg/scope/machine.go @@ -1,5 +1,5 @@ /* - * Copyright 2024 IONOS Cloud. + * Copyright 2023-2024 IONOS Cloud. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From e9d9f8302e45793fd632b8bfade572afce84f2b0 Mon Sep 17 00:00:00 2001 From: Gustavo Alves Date: Mon, 8 Jan 2024 17:45:31 +0100 Subject: [PATCH 3/5] Lint fixes --- api/v1alpha1/ionoscloudcluster_types.go | 6 ++--- api/v1alpha1/zz_generated.deepcopy.go | 23 ++++++++++++++----- .../ionoscloudmachine_controller.go | 19 ++++++++------- internal/ionoscloud/client.go | 2 ++ internal/ionoscloud/client/client.go | 2 ++ pkg/scope/machine.go | 11 ++++++--- 6 files changed, 41 insertions(+), 22 deletions(-) diff --git a/api/v1alpha1/ionoscloudcluster_types.go b/api/v1alpha1/ionoscloudcluster_types.go index fe3b341c..a1e359dc 100644 --- a/api/v1alpha1/ionoscloudcluster_types.go +++ b/api/v1alpha1/ionoscloudcluster_types.go @@ -55,11 +55,11 @@ type IonosCloudClusterStatus struct { // +optional Conditions clusterv1.Conditions `json:"conditions,omitempty"` - PendingRequests PendingRequests `json:"PendingRequests,omitEmpty"` + // PendingRequests is a map that maps data centers IDs with a pending provisioning request made during reconciliation. + // +optional + PendingRequests map[string]*ProvisioningRequest `json:"pendingRequests,omitempty"` } -type PendingRequests map[string]*ProvisioningRequest - //+kubebuilder:object:root=true //+kubebuilder:subresource:status //+kubebuilder:printcolumn:name="Cluster",type="string",JSONPath=".metadata.labels['cluster\\.x-k8s\\.io/cluster-name']",description="Cluster" diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index b1080c08..86dd37f4 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -1,7 +1,7 @@ //go:build !ignore_autogenerated /* -Copyright 2023 IONOS Cloud. +Copyright 2023-2024 IONOS Cloud. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -111,6 +111,22 @@ func (in *IonosCloudClusterStatus) DeepCopyInto(out *IonosCloudClusterStatus) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.PendingRequests != nil { + in, out := &in.PendingRequests, &out.PendingRequests + *out = make(map[string]*ProvisioningRequest, len(*in)) + for key, val := range *in { + var outVal *ProvisioningRequest + if val == nil { + (*out)[key] = nil + } else { + inVal := (*in)[key] + in, out := &inVal, &outVal + *out = new(ProvisioningRequest) + **out = **in + } + (*out)[key] = outVal + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IonosCloudClusterStatus. @@ -227,11 +243,6 @@ func (in *IonosCloudMachineStatus) DeepCopyInto(out *IonosCloudMachineStatus) { (*in)[i].DeepCopyInto(&(*out)[i]) } } - if in.CurrentRequest != nil { - in, out := &in.CurrentRequest, &out.CurrentRequest - *out = new(ProvisioningRequest) - **out = **in - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IonosCloudMachineStatus. diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index 59a1879d..05d1550d 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -20,22 +20,21 @@ import ( "context" "errors" "fmt" + "net/http" + "github.com/go-logr/logr" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/pkg/scope" sdk "github.com/ionos-cloud/sdk-go/v6" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/klog/v2" "k8s.io/utils/pointer" - "net/http" - "sigs.k8s.io/cluster-api/util/annotations" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" "sigs.k8s.io/cluster-api/util" - "sigs.k8s.io/controller-runtime/pkg/handler" - - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/cluster-api/util/annotations" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/ionoscloud" @@ -88,7 +87,7 @@ func (r *IonosCloudMachineReconciler) Reconcile(ctx context.Context, req ctrl.Re cluster, err := util.GetClusterFromMetadata(ctx, r.Client, machine.ObjectMeta) if err != nil { logger.Info("machine is missing cluster label or cluster does not exist") - return ctrl.Result{}, nil + return ctrl.Result{}, err } if annotations.IsPaused(cluster, ionosCloudMachine) { @@ -122,11 +121,11 @@ func (r *IonosCloudMachineReconciler) Reconcile(ctx context.Context, req ctrl.Re } //// Always close the scope when exiting this function, so we can persist any ProxmoxMachine changes. - //defer func() { + // defer func() { // if err := machineScope.Close(); err != nil && reterr == nil { // reterr = err // } - //}() + // }() if !ionosCloudMachine.ObjectMeta.DeletionTimestamp.IsZero() { return r.reconcileDelete(ctx, machineScope) diff --git a/internal/ionoscloud/client.go b/internal/ionoscloud/client.go index a97a925b..aecd2858 100644 --- a/internal/ionoscloud/client.go +++ b/internal/ionoscloud/client.go @@ -61,4 +61,6 @@ type Client interface { GetVolume(ctx context.Context, dataCenterID, volumeID string) (*ionoscloud.Volume, error) // DestroyVolume deletes the volume that matches volumeID in the specified data center. DestroyVolume(ctx context.Context, dataCenterID, volumeID string) error + // WaitForRequest waits for the completion of the provided request, return an error if it fails. + WaitForRequest(ctx context.Context, requestURL string) error } diff --git a/internal/ionoscloud/client/client.go b/internal/ionoscloud/client/client.go index abd6f1ef..5da2f5bb 100644 --- a/internal/ionoscloud/client/client.go +++ b/internal/ionoscloud/client/client.go @@ -282,6 +282,7 @@ func (c *IonosCloudClient) DestroyVolume(ctx context.Context, dataCenterID, volu return nil } +// CheckRequestStatus returns the status of a request and an error if checking for it fails. func (c *IonosCloudClient) CheckRequestStatus(ctx context.Context, requestURL string) (*sdk.RequestStatus, error) { if requestURL == "" { return nil, errRequestURLIsEmpty @@ -293,6 +294,7 @@ func (c *IonosCloudClient) CheckRequestStatus(ctx context.Context, requestURL st return requestStatus, nil } +// WaitForRequest waits for the completion of the provided request, return an error if it fails. func (c *IonosCloudClient) WaitForRequest(ctx context.Context, requestURL string) error { if requestURL == "" { return errRequestURLIsEmpty diff --git a/pkg/scope/machine.go b/pkg/scope/machine.go index 2aafa33b..24281e6c 100644 --- a/pkg/scope/machine.go +++ b/pkg/scope/machine.go @@ -21,14 +21,17 @@ import ( "context" "errors" "fmt" + "github.com/go-logr/logr" - "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" "sigs.k8s.io/cluster-api/util/patch" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" + + infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" ) +// MachineScope defines a basic context for primary use in IonosCloudMachineReconciler. type MachineScope struct { *logr.Logger @@ -38,18 +41,20 @@ type MachineScope struct { Machine *clusterv1.Machine ClusterScope *ClusterScope - IonosCloudMachine *v1alpha1.IonosCloudMachine + IonosCloudMachine *infrav1.IonosCloudMachine } +// MachineScopeParams is a struct that contains the params used to create a new MachineScope through NewMachineScope. type MachineScopeParams struct { Client client.Client Logger *logr.Logger Cluster *clusterv1.Cluster Machine *clusterv1.Machine InfraCluster *ClusterScope - IonosCloudMachine *v1alpha1.IonosCloudMachine + IonosCloudMachine *infrav1.IonosCloudMachine } +// NewMachineScope creates a new MachineScope using the provided params. func NewMachineScope(params MachineScopeParams) (*MachineScope, error) { if params.Client == nil { return nil, errors.New("machine scope params lack a client") From a6f6217ee3f8ae3522fc26ffd197b02be3b5c6db Mon Sep 17 00:00:00 2001 From: Gustavo Alves Date: Mon, 8 Jan 2024 17:48:11 +0100 Subject: [PATCH 4/5] Lint fixes, pt. 2 --- internal/controller/ionoscloudmachine_controller.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/controller/ionoscloudmachine_controller.go b/internal/controller/ionoscloudmachine_controller.go index 05d1550d..56a4e337 100644 --- a/internal/controller/ionoscloudmachine_controller.go +++ b/internal/controller/ionoscloudmachine_controller.go @@ -23,7 +23,6 @@ import ( "net/http" "github.com/go-logr/logr" - "github.com/ionos-cloud/cluster-api-provider-ionoscloud/pkg/scope" sdk "github.com/ionos-cloud/sdk-go/v6" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" @@ -38,6 +37,7 @@ import ( infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/ionoscloud" + "github.com/ionos-cloud/cluster-api-provider-ionoscloud/pkg/scope" ) // IonosCloudMachineReconciler reconciles a IonosCloudMachine object. From 27b367968e5c3c3042a1d7bc2c69fe9645ae4ea8 Mon Sep 17 00:00:00 2001 From: Gustavo Alves Date: Mon, 8 Jan 2024 17:50:24 +0100 Subject: [PATCH 5/5] Updated manifests --- ...e.cluster.x-k8s.io_ionoscloudclusters.yaml | 31 +++++++++++++++++++ ...e.cluster.x-k8s.io_ionoscloudmachines.yaml | 27 ---------------- 2 files changed, 31 insertions(+), 27 deletions(-) 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 cbfd3546..acf0f60e 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudclusters.yaml @@ -120,6 +120,37 @@ spec: - type type: object type: array + pendingRequests: + additionalProperties: + description: ProvisioningRequest is a definition of a provisioning + request in the IONOS Cloud. + properties: + failureMessage: + description: Message is the request message, which can also + contain error information. + type: string + method: + description: Method is the request method + type: string + requestPath: + description: RequestPath is the sub path for the request URL + type: string + state: + description: RequestStatus is the status of the request in the + queue. + enum: + - QUEUED + - RUNNING + - DONE + - FAILED + type: string + required: + - method + - requestPath + type: object + description: PendingRequests is a map that maps data centers IDs with + a pending provisioning request made during reconciliation. + type: object ready: default: false description: Ready indicates that the cluster is ready. diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml index ac87f258..6b830f99 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ionoscloudmachines.yaml @@ -175,33 +175,6 @@ spec: - type type: object type: array - currentRequest: - description: CurrentRequest shows the current provisioning request - for any cloud resource, that is being created. - properties: - failureMessage: - description: Message is the request message, which can also contain - error information. - type: string - method: - description: Method is the request method - type: string - requestPath: - description: RequestPath is the sub path for the request URL - type: string - state: - description: RequestStatus is the status of the request in the - queue. - enum: - - QUEUED - - RUNNING - - DONE - - FAILED - type: string - required: - - method - - requestPath - type: object failureMessage: description: "FailureMessage will be set in the event that there is a terminal problem reconciling the Machine and will contain a more