Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🐛 Harden deletion logic #111

Merged
merged 8 commits into from
May 3, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions api/v1alpha1/ionoscloudcluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ const (
// associated with the IonosCloudCluster before removing it from the API server.
ClusterFinalizer = "ionoscloudcluster.infrastructure.cluster.x-k8s.io"

// ClusterCredentialFinalizer allows cleanup of resources, which are
// associated with the IonosCloudCluster credentials before removing it from the API server.
ClusterCredentialFinalizer = ClusterFinalizer + "/credentials"
jriedel-ionos marked this conversation as resolved.
Show resolved Hide resolved

// IonosCloudClusterReady is the condition for the IonosCloudCluster, which indicates that the cluster is ready.
IonosCloudClusterReady clusterv1.ConditionType = "ClusterReady"

Expand Down
16 changes: 14 additions & 2 deletions internal/controller/ionoscloudcluster_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,16 @@ func (r *IonosCloudClusterReconciler) reconcileDelete(
return ctrl.Result{RequeueAfter: defaultReconcileDuration}, nil
}

// TODO(lubedacht): check if there are any more machine CRs existing.
// If there are requeue with an offset.
machines, err := clusterScope.ListMachinesForCluster(ctx, nil)
if err != nil {
log.Error(err, "unable to list machines for cluster")
return ctrl.Result{}, err
piepmatz marked this conversation as resolved.
Show resolved Hide resolved
}

if len(machines) > 0 {
log.Info("Waiting for all IonosCloudMachines to be deleted", "remaining", len(machines))
return ctrl.Result{RequeueAfter: defaultReconcileDuration}, nil
}

reconcileSequence := []serviceReconcileStep[scope.Cluster]{
{"ReconcileControlPlaneEndpointDeletion", cloudService.ReconcileControlPlaneEndpointDeletion},
Expand All @@ -186,6 +194,10 @@ func (r *IonosCloudClusterReconciler) reconcileDelete(
return ctrl.Result{RequeueAfter: defaultReconcileDuration}, err
}
}
if err := removeCredentialFinalizer(ctx, r.Client, clusterScope.IonosCluster); err != nil {
log.Error(err, "unable to remove finalizer from IonosCloudCluster")
return ctrl.Result{}, err
}
controllerutil.RemoveFinalizer(clusterScope.IonosCluster, infrav1.ClusterFinalizer)
return ctrl.Result{}, nil
}
Expand Down
45 changes: 45 additions & 0 deletions internal/controller/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
sdk "github.com/ionos-cloud/sdk-go/v6"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1"
icc "github.com/ionos-cloud/cluster-api-provider-ionoscloud/internal/ionoscloud/client"
Expand Down Expand Up @@ -80,6 +81,10 @@ func createServiceFromCluster(
return nil, err
}

if err := ensureSecretControlledByCluster(ctx, c, cluster, &authSecret); err != nil {
return nil, err
}

token := string(authSecret.Data["token"])
apiURL := string(authSecret.Data["apiURL"])
caBundle := authSecret.Data["caBundle"]
Expand All @@ -91,3 +96,43 @@ func createServiceFromCluster(

return cloud.NewService(ionosClient, log)
}

// ensureSecretControlledByCluster ensures that the secrets will contain a finalizer and a controller reference.
// The secret should only be deleted when there are no resources left in the IONOS Cloud environment.
func ensureSecretControlledByCluster(
ctx context.Context, c client.Client,
cluster *infrav1.IonosCloudCluster,
secret *corev1.Secret,
) error {
requireUpdate := controllerutil.AddFinalizer(secret, infrav1.ClusterCredentialFinalizer)

if !controllerutil.HasControllerReference(secret) {
if err := controllerutil.SetControllerReference(cluster, secret, c.Scheme()); err != nil {
return err
}
requireUpdate = true
}

if requireUpdate {
return c.Update(ctx, secret)
}

return nil
}

// removeCredentialFinalizer removes the finalizer from the credential secret.
func removeCredentialFinalizer(ctx context.Context, c client.Client, cluster *infrav1.IonosCloudCluster) error {
secretKey := client.ObjectKey{
Namespace: cluster.Namespace,
Name: cluster.Spec.CredentialsRef.Name,
}
var secret corev1.Secret

if err := c.Get(ctx, secretKey, &secret); err != nil {
// If the secret does not exist anymore, there is nothing we can do.
return client.IgnoreNotFound(err)
}

controllerutil.RemoveFinalizer(&secret, infrav1.ClusterCredentialFinalizer)
return c.Update(ctx, &secret)
}
22 changes: 22 additions & 0 deletions scope/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (

// Cluster defines a basic cluster context for primary use in IonosCloudClusterReconciler.
type Cluster struct {
client client.Client
patchHelper *patch.Helper
Cluster *clusterv1.Cluster
IonosCluster *infrav1.IonosCloudCluster
Expand Down Expand Up @@ -67,6 +68,7 @@ func NewCluster(params ClusterParams) (*Cluster, error) {
}

clusterScope := &Cluster{
client: params.Client,
Cluster: params.Cluster,
IonosCluster: params.IonosCluster,
patchHelper: helper,
Expand All @@ -85,6 +87,26 @@ func (c *Cluster) SetControlPlaneEndpointIPBlockID(id string) {
c.IonosCluster.Status.ControlPlaneEndpointIPBlockID = id
}

// ListMachinesForCluster returns a list of IonosCloudMachines in the same namespace and with the same cluster label.
// With machineLabels, additional search labels can be provided.
func (c *Cluster) ListMachinesForCluster(
piepmatz marked this conversation as resolved.
Show resolved Hide resolved
ctx context.Context,
machineLabels client.MatchingLabels,
) ([]infrav1.IonosCloudMachine, error) {
if machineLabels == nil {
machineLabels = client.MatchingLabels{}
}

machineLabels[clusterv1.ClusterNameLabel] = c.Cluster.Name
listOpts := []client.ListOption{client.InNamespace(c.Cluster.Namespace), machineLabels}

machineList := &infrav1.IonosCloudMachineList{}
if err := c.client.List(ctx, machineList, listOpts...); err != nil {
return nil, err
}
return machineList.Items, nil
}

// Location is a shortcut for getting the location used by the IONOS Cloud cluster IP block.
func (c *Cluster) Location() string {
return c.IonosCluster.Spec.Location
Expand Down
114 changes: 114 additions & 0 deletions scope/cluster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@ limitations under the License.
package scope

import (
"context"
"testing"

"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"

infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1"
Expand Down Expand Up @@ -85,3 +88,114 @@ func TestNewClusterMissingParams(t *testing.T) {
})
}
}

func TestClusterListMachinesForCluster(t *testing.T) {
scheme := runtime.NewScheme()
require.NoError(t, infrav1.AddToScheme(scheme))

const clusterName = "test-cluster"

makeLabels := func(clusterName string, additionalLabels map[string]string) map[string]string {
if additionalLabels == nil {
return map[string]string{clusterv1.ClusterNameLabel: clusterName}
}

additionalLabels[clusterv1.ClusterNameLabel] = clusterName
return additionalLabels
}

tests := []struct {
name string
initialObjects []client.Object
searchLabels client.MatchingLabels
expectedNames map[string]struct{}
piepmatz marked this conversation as resolved.
Show resolved Hide resolved
}{{
name: "List all machines for a cluster",
initialObjects: []client.Object{
buildMachineWithLabel("machine-1", makeLabels(clusterName, nil)),
buildMachineWithLabel("machine-2", makeLabels(clusterName, nil)),
buildMachineWithLabel("machine-3", makeLabels(clusterName, nil)),
},
searchLabels: client.MatchingLabels{},
expectedNames: map[string]struct{}{
"machine-1": {},
"machine-2": {},
"machine-3": {},
},
}, {
name: "List only machines with specific labels",
initialObjects: []client.Object{
buildMachineWithLabel("machine-1", makeLabels(clusterName, map[string]string{"foo": "bar"})),
buildMachineWithLabel("machine-2", makeLabels(clusterName, map[string]string{"foo": "bar"})),
buildMachineWithLabel("machine-3", makeLabels(clusterName, nil)),
},
searchLabels: client.MatchingLabels{
"foo": "bar",
},
expectedNames: map[string]struct{}{
"machine-1": {},
"machine-2": {},
},
}, {
name: "List no machines",
initialObjects: []client.Object{
buildMachineWithLabel("machine-1", makeLabels(clusterName, map[string]string{"foo": "notbar"})),
buildMachineWithLabel("machine-2", makeLabels(clusterName, map[string]string{"foo": "notbar"})),
buildMachineWithLabel("machine-3", makeLabels(clusterName, map[string]string{"foo": "notbar"})),
},
searchLabels: makeLabels(clusterName, map[string]string{"foo": "bar"}),
expectedNames: map[string]struct{}{},
}}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
params := ClusterParams{
Cluster: &clusterv1.Cluster{
ObjectMeta: metav1.ObjectMeta{
Name: clusterName,
Namespace: metav1.NamespaceDefault,
Labels: map[string]string{
clusterv1.ClusterNameLabel: clusterName,
},
},
},
IonosCluster: &infrav1.IonosCloudCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "test-ionos-cluster",
Namespace: metav1.NamespaceDefault,
Labels: map[string]string{
clusterv1.ClusterNameLabel: clusterName,
},
},
Status: infrav1.IonosCloudClusterStatus{},
},
}

cl := fake.NewClientBuilder().WithScheme(scheme).
WithObjects(test.initialObjects...).Build()

params.Client = cl
cs, err := NewCluster(params)
require.NoError(t, err)
require.NotNil(t, cs)

machines, err := cs.ListMachinesForCluster(context.Background(), test.searchLabels)
require.NoError(t, err)
require.Len(t, machines, len(test.expectedNames))

for _, m := range machines {
require.Contains(t, test.expectedNames, m.Name)
}
})
}
}

func buildMachineWithLabel(name string, labels map[string]string) *infrav1.IonosCloudMachine {
return &infrav1.IonosCloudMachine{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: metav1.NamespaceDefault,
Labels: labels,
},
}
}
34 changes: 8 additions & 26 deletions scope/machine.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,24 +122,12 @@ func (m *Machine) CountMachines(ctx context.Context, machineLabels client.Matchi
return len(machines), err
}

// ListMachines returns a list of IonosCloudMachines in the same namespace and with the same cluster label.
// With machineLabels, additional search labels can be provided.
// ListMachines is a convenience wrapper function for the Cluster.ListMachinesForCluster function.
func (m *Machine) ListMachines(
ctx context.Context,
machineLabels client.MatchingLabels,
) ([]infrav1.IonosCloudMachine, error) {
if machineLabels == nil {
machineLabels = client.MatchingLabels{}
}

machineLabels[clusterv1.ClusterNameLabel] = m.ClusterScope.Cluster.Name
listOpts := []client.ListOption{client.InNamespace(m.IonosMachine.Namespace), machineLabels}

machineList := &infrav1.IonosCloudMachineList{}
if err := m.client.List(ctx, machineList, listOpts...); err != nil {
return nil, err
}
return machineList.Items, nil
return m.ClusterScope.ListMachinesForCluster(ctx, machineLabels)
}

// FindLatestMachine returns the latest IonosCloudMachine in the same namespace
Expand All @@ -151,23 +139,17 @@ func (m *Machine) FindLatestMachine(
ctx context.Context,
matchLabels client.MatchingLabels,
) (*infrav1.IonosCloudMachine, error) {
if matchLabels == nil {
matchLabels = client.MatchingLabels{}
}

matchLabels[clusterv1.ClusterNameLabel] = m.ClusterScope.Cluster.Name
listOpts := []client.ListOption{client.InNamespace(m.IonosMachine.Namespace), matchLabels}

machineList := &infrav1.IonosCloudMachineList{}
if err := m.client.List(ctx, machineList, listOpts...); err != nil {
machines, err := m.ClusterScope.ListMachinesForCluster(ctx, matchLabels)
if err != nil {
return nil, err
}
if len(machineList.Items) <= 1 {

if len(machines) <= 1 {
return nil, nil
}

latestMachine := machineList.Items[0]
for _, machine := range machineList.Items {
latestMachine := machines[0]
for _, machine := range machines {
if !machine.CreationTimestamp.Before(&latestMachine.CreationTimestamp) && machine.Name != m.IonosMachine.Name {
latestMachine = machine
}
Expand Down
4 changes: 3 additions & 1 deletion scope/machine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,12 @@ func exampleParams(t *testing.T) MachineParams {
if err := infrav1.AddToScheme(scheme.Scheme); err != nil {
require.NoError(t, err, "could not construct params")
}
cl := fake.NewClientBuilder().WithScheme(scheme.Scheme).Build()
return MachineParams{
Client: fake.NewClientBuilder().WithScheme(scheme.Scheme).Build(),
Client: cl,
Machine: &clusterv1.Machine{},
ClusterScope: &Cluster{
client: cl,
Cluster: &clusterv1.Cluster{},
},
IonosMachine: &infrav1.IonosCloudMachine{},
Expand Down
Loading