Skip to content

Commit a644f1a

Browse files
authored
fix: deletion defects and resource orphans (#17)
1 parent 4445bf3 commit a644f1a

16 files changed

Lines changed: 580 additions & 279 deletions

‎README.md‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,13 @@ Check out the [Quick Start](docs/src/quick-start.md) for launching a cluster on
8181
This provider's versions are compatible with the following versions of Cluster API
8282
and support all Kubernetes versions that is supported by its compatible Cluster API version:
8383

84-
| | Cluster API v1alpha4 (v0.4) | Cluster API v1beta1 (v1.x) |
85-
| ------------------------ | :-------------------------: | :------------------------: |
86-
| CAPSTK v1alpha1 `(main)` | x | ✓ |
84+
| | Cluster API v1alpha4 (v0.4) | Cluster API v1beta1 | Cluster API v1beta2 |
85+
| ------------------------ | :-------------------------: | :-----------------: | :-----------------: |
86+
| CAPSTK v1alpha1 `(main)` | x | x | ✓ |
87+
88+
This provider implements the **v1beta2** contract, as declared in
89+
[`metadata.yaml`](metadata.yaml), and is built against `sigs.k8s.io/cluster-api`
90+
v1.13.2.
8791

8892
(See [Kubernetes support matrix](https://cluster-api.sigs.k8s.io/reference/versions.html) of Cluster API versions).
8993

‎cloud/fake/client.go‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ type Client struct {
5555
FailNextEnsureNodeSSH error
5656
FailNextDeleteNodeSSH error
5757

58+
// Before* hooks, if non-nil, run before the call they belong to. They let a
59+
// test observe API server state at the exact moment a cloud call would
60+
// happen. Unlike FailNext*, they are not consumed.
61+
BeforeCreateServer func()
62+
BeforeGetNetwork func()
63+
5864
// CreateServerCalls counts successful CreateServer calls (for idempotency
5965
// assertions).
6066
CreateServerCalls int
@@ -171,6 +177,9 @@ func (c *Client) CreateServer(_ context.Context, input cloud.CreateServerInput)
171177
c.mu.Lock()
172178
defer c.mu.Unlock()
173179

180+
if c.BeforeCreateServer != nil {
181+
c.BeforeCreateServer()
182+
}
174183
if err := consume(&c.FailNextCreateServer); err != nil {
175184
return nil, err
176185
}
@@ -215,6 +224,9 @@ func (c *Client) GetNetwork(_ context.Context, id string) (*cloud.Network, error
215224
c.mu.Lock()
216225
defer c.mu.Unlock()
217226

227+
if c.BeforeGetNetwork != nil {
228+
c.BeforeGetNetwork()
229+
}
218230
if err := consume(&c.FailNextGetNetwork); err != nil {
219231
return nil, err
220232
}

‎controller/constants.go‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,11 @@ const (
1818
cloudInitRefKindSecret = "Secret"
1919

2020
retryableErrorRequeueAfter = 5 * time.Second
21+
22+
// deleteRequeueAfter paces the wait for dependent objects to disappear during deletion.
23+
deleteRequeueAfter = 5 * time.Second
24+
25+
// credentialsRetryRequeueAfter paces retries of invalid credentials, which
26+
// need an operator to fix the Secret before they can succeed.
27+
credentialsRetryRequeueAfter = time.Minute
2128
)

‎controller/controller_test_helpers_test.go‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -87,23 +87,20 @@ func createOwnerCluster(ctx context.Context, name string) {
8787
Expect(k8sClient.Create(ctx, cluster)).To(Succeed())
8888
}
8989

90-
func createOwnerMachine(ctx context.Context, name, namespace, clusterName, stackitMachineName string, bootstrapSecretName *string) {
91-
if bootstrapSecretName == nil {
92-
empty := ""
93-
bootstrapSecretName = &empty
94-
}
90+
func createOwnerMachine(ctx context.Context, name, clusterName, stackitMachineName string) {
9591
machine := &clusterv1.Machine{
9692
ObjectMeta: metav1.ObjectMeta{
9793
Name: name,
98-
Namespace: namespace,
94+
Namespace: "default",
9995
Labels: map[string]string{
10096
clusterv1.ClusterNameLabel: clusterName,
10197
},
10298
},
10399
Spec: clusterv1.MachineSpec{
104100
ClusterName: clusterName,
105101
Bootstrap: clusterv1.Bootstrap{
106-
DataSecretName: bootstrapSecretName,
102+
// Specs that need bootstrap data attach it with updateMachineBootstrapSecret.
103+
DataSecretName: new(""),
107104
},
108105
InfrastructureRef: clusterv1.ContractVersionedObjectReference{
109106
APIGroup: infrav1.GroupVersion.Group,

‎controller/stackitcluster_bastion.go‎

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -39,33 +39,30 @@ func (r *StackitClusterReconciler) reconcileBastion(
3939
cloudClient cloud.Client,
4040
clusterScope *scope.ClusterScope,
4141
) (ctrl.Result, bool, error) {
42-
cluster := clusterScope.StackitCluster
43-
input := bastionservice.Input(cluster, nil)
42+
stackitCluster := clusterScope.StackitCluster
43+
input := bastionservice.Input(stackitCluster, nil)
4444
status := cloud.Bastion{
45-
ServerID: cluster.Status.Bastion.ServerID,
46-
PublicIPID: cluster.Status.Bastion.PublicIPID,
47-
PublicIP: cluster.Status.Bastion.PublicIP,
48-
SecurityGroupID: cluster.Status.Bastion.SecurityGroupID,
49-
}
50-
51-
if !cluster.Spec.Bastion.Enabled {
52-
// The condition carries this reason only after a cleanup has succeeded,
53-
// so anything else means we may still own bastion resources — including
54-
// the case where EnsureBastion succeeded but its status patch was lost.
55-
// Keying on it instead of on the status keeps the tag-based cleanup to
56-
// once per cluster rather than once per reconcile, which matters because
57-
// this path runs for every cluster without a bastion.
58-
condition := meta.FindStatusCondition(cluster.Status.Conditions, infrav1.ClusterBastionReadyCondition)
45+
ServerID: stackitCluster.Status.Bastion.ServerID,
46+
PublicIPID: stackitCluster.Status.Bastion.PublicIPID,
47+
PublicIP: stackitCluster.Status.Bastion.PublicIP,
48+
SecurityGroupID: stackitCluster.Status.Bastion.SecurityGroupID,
49+
}
50+
51+
if !stackitCluster.Spec.Bastion.Enabled {
52+
// The reason is set only after a cleanup has succeeded, so anything else
53+
// means bastion resources may still exist. Keying on it rather than on
54+
// the status keeps the tag-based cleanup to once per cluster.
55+
condition := meta.FindStatusCondition(stackitCluster.Status.Conditions, infrav1.ClusterBastionReadyCondition)
5956
if condition == nil || condition.Reason != bastionDisabledReason {
60-
if err := cloudClient.DeleteNodeSSHAccess(ctx, bastionservice.NodeSSHAccessTags(cluster)); err != nil {
57+
if err := cloudClient.DeleteNodeSSHAccess(ctx, bastionservice.NodeSSHAccessTags(stackitCluster)); err != nil {
6158
return ctrl.Result{}, false, err
6259
}
6360
if err := cloudClient.DeleteBastion(ctx, input, status); err != nil {
6461
return ctrl.Result{}, false, err
6562
}
6663
clusterScope.ClearBastionStatus()
6764
if r.Recorder != nil {
68-
r.Recorder.Eventf(cluster, nil, corev1.EventTypeNormal, "BastionDeleted", "Delete", "Deleted bastion")
65+
r.Recorder.Eventf(stackitCluster, nil, corev1.EventTypeNormal, "BastionDeleted", "Delete", "Deleted bastion")
6966
}
7067
}
7168
clusterScope.SetConditions(
@@ -77,7 +74,7 @@ func (r *StackitClusterReconciler) reconcileBastion(
7774
return ctrl.Result{}, true, nil
7875
}
7976

80-
if err := validateBastionSpec(cluster.Spec.Bastion); err != nil {
77+
if err := validateBastionSpec(stackitCluster.Spec.Bastion); err != nil {
8178
clusterScope.SetNotReady(
8279
"InvalidBastionSpec",
8380
err.Error(),
@@ -87,7 +84,7 @@ func (r *StackitClusterReconciler) reconcileBastion(
8784
return ctrl.Result{}, false, nil
8885
}
8986

90-
cloudInit, err := r.resolveBastionCloudInit(ctx, cluster)
87+
cloudInit, err := r.resolveBastionCloudInit(ctx, stackitCluster)
9188
if err != nil {
9289
clusterScope.SetNotReady(
9390
"CloudInitRefError",
@@ -99,33 +96,38 @@ func (r *StackitClusterReconciler) reconcileBastion(
9996
}
10097
input.CloudInit = cloudInit
10198

102-
if bastionNeedsRecreate(cluster, cloudInit) {
103-
if err := cloudClient.DeleteNodeSSHAccess(ctx, bastionservice.NodeSSHAccessTags(cluster)); err != nil && !cloud.IsNotFound(err) {
99+
if bastionNeedsRecreate(stackitCluster, cloudInit) {
100+
if err := cloudClient.DeleteNodeSSHAccess(ctx, bastionservice.NodeSSHAccessTags(stackitCluster)); err != nil && !cloud.IsNotFound(err) {
104101
return ctrl.Result{}, false, err
105102
}
106103
if err := cloudClient.DeleteBastion(ctx, input, status); err != nil && !cloud.IsNotFound(err) {
107104
return ctrl.Result{}, false, err
108105
}
109106
clusterScope.ClearBastionStatus()
110-
clusterScope.SetNotReady("Recreating", "recreating bastion because cloudInitRef content changed", infrav1.ClusterBastionReadyCondition, infrav1.ClusterReadyCondition)
107+
clusterScope.SetNotReady(
108+
"Recreating",
109+
"recreating bastion because cloudInitRef content changed",
110+
infrav1.ClusterBastionReadyCondition,
111+
infrav1.ClusterReadyCondition,
112+
)
111113
if r.Recorder != nil {
112114
r.Recorder.Eventf(
113-
cluster, nil, corev1.EventTypeNormal, "BastionRecreating", "Recreate",
115+
stackitCluster, nil, corev1.EventTypeNormal, "BastionRecreating", "Recreate",
114116
"Recreating bastion because cloudInitRef content changed",
115117
)
116118
}
117119
return ctrl.Result{RequeueAfter: retryableErrorRequeueAfter}, false, nil
118120
}
119121

120-
hadBastionStatus := hasBastionStatus(cluster.Status.Bastion)
122+
hadBastionStatus := hasBastionStatus(stackitCluster.Status.Bastion)
121123
bastion, err := cloudClient.EnsureBastion(ctx, input)
122124
if err != nil {
123125
return ctrl.Result{}, false, err
124126
}
125127
clusterScope.SetBastionStatus(bastion, bastionCloudInitHash(cloudInit))
126128
if !hadBastionStatus && r.Recorder != nil {
127129
r.Recorder.Eventf(
128-
cluster, nil, corev1.EventTypeNormal, "BastionCreated", "Create", "Created bastion %s", bastion.ServerID,
130+
stackitCluster, nil, corev1.EventTypeNormal, "BastionCreated", "Create", "Created bastion %s", bastion.ServerID,
129131
)
130132
}
131133
if bastion.ServerState != "" && bastion.ServerState != "ACTIVE" {
@@ -180,11 +182,11 @@ func hasBastionStatus(status infrav1.StackitBastionStatus) bool {
180182
return status.ServerID != "" || status.PublicIPID != "" || status.PublicIP != "" || status.SecurityGroupID != ""
181183
}
182184

183-
func bastionNeedsRecreate(sc *infrav1.StackitCluster, cloudInit []byte) bool {
184-
if !hasBastionStatus(sc.Status.Bastion) {
185+
func bastionNeedsRecreate(stackitCluster *infrav1.StackitCluster, cloudInit []byte) bool {
186+
if !hasBastionStatus(stackitCluster.Status.Bastion) {
185187
return false
186188
}
187-
return sc.Status.Bastion.CloudInitHash != bastionCloudInitHash(cloudInit)
189+
return stackitCluster.Status.Bastion.CloudInitHash != bastionCloudInitHash(cloudInit)
188190
}
189191

190192
func bastionCloudInitHash(cloudInit []byte) string {
@@ -194,12 +196,12 @@ func bastionCloudInitHash(cloudInit []byte) string {
194196
return fmt.Sprintf("%x", sha256.Sum256(cloudInit))
195197
}
196198

197-
func (r *StackitClusterReconciler) resolveBastionCloudInit(ctx context.Context, sc *infrav1.StackitCluster) ([]byte, error) {
198-
ref := sc.Spec.Bastion.CloudInitRef
199+
func (r *StackitClusterReconciler) resolveBastionCloudInit(ctx context.Context, stackitCluster *infrav1.StackitCluster) ([]byte, error) {
200+
ref := stackitCluster.Spec.Bastion.CloudInitRef
199201
if ref == nil {
200202
return nil, nil
201203
}
202-
key := types.NamespacedName{Namespace: sc.Namespace, Name: ref.Name}
204+
key := types.NamespacedName{Namespace: stackitCluster.Namespace, Name: ref.Name}
203205
switch ref.Kind {
204206
case "ConfigMap":
205207
configMap := &corev1.ConfigMap{}

‎controller/stackitcluster_controller.go‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ type StackitClusterReconciler struct {
5454
// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=stackitclusters/status,verbs=get;update;patch
5555
// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=stackitclusters/finalizers,verbs=update
5656
// +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters,verbs=get;list;watch
57+
// +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=machines,verbs=get;list;watch
5758
// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch
5859
// +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch
5960
// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch
@@ -94,7 +95,7 @@ func (r *StackitClusterReconciler) Reconcile(ctx context.Context, req ctrl.Reque
9495
util.SetPausedCondition(&stackitCluster.Status.Conditions, stackitCluster.Generation, false, "")
9596

9697
if !stackitCluster.DeletionTimestamp.IsZero() {
97-
return ctrl.Result{}, r.reconcileDelete(ctx, clusterScope)
98+
return r.reconcileDelete(ctx, clusterScope)
9899
}
99100
return r.reconcileNormal(ctx, clusterScope)
100101
}

0 commit comments

Comments
 (0)