Skip to content

Commit

Permalink
Add LAN provisioning without tests
Browse files Browse the repository at this point in the history
  • Loading branch information
gfariasalves-ionos committed Jan 8, 2024
1 parent 3459f0e commit 1b820ba
Show file tree
Hide file tree
Showing 9 changed files with 440 additions and 24 deletions.
2 changes: 2 additions & 0 deletions .codespellignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
capi
capic
decorder
reterr
ionos
4 changes: 4 additions & 0 deletions api/v1alpha1/ionoscloudcluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`

Check failure on line 58 in api/v1alpha1/ionoscloudcluster_types.go

View workflow job for this annotation

GitHub Actions / go_test

SA5008: unknown JSON option "omitEmpty" (staticcheck)
}

type PendingRequests map[string]*ProvisioningRequest

Check failure on line 61 in api/v1alpha1/ionoscloudcluster_types.go

View workflow job for this annotation

GitHub Actions / go_test

exported: exported type PendingRequests should have comment or be unexported (revive)

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
//+kubebuilder:printcolumn:name="Cluster",type="string",JSONPath=".metadata.labels['cluster\\.x-k8s\\.io/cluster-name']",description="Cluster"
Expand Down
4 changes: 0 additions & 4 deletions api/v1alpha1/ionoscloudmachine_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion hack/boilerplate.go.txt
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
306 changes: 303 additions & 3 deletions internal/controller/ionoscloudmachine_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -52,17 +61,103 @@ type IonosCloudMachineReconciler struct {
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/[email protected]/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) {
return ctrl.Result{}, nil
}
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

Check failure on line 91 in internal/controller/ionoscloudmachine_controller.go

View workflow job for this annotation

GitHub Actions / go_test

error is not nil (line 88) but it returns nil (nilerr)
}

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
}

Expand All @@ -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
}
7 changes: 4 additions & 3 deletions internal/ionoscloud/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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.
Expand Down
Loading

0 comments on commit 1b820ba

Please sign in to comment.