Skip to content

Commit 9ac9b40

Browse files
committed
controllers: adopt ownerReferences for existing resources to ensure cascading deletes; bump VERSION to 0.4.8 and update CHANGELOG
1 parent f9eec56 commit 9ac9b40

File tree

7 files changed

+135
-4
lines changed

7 files changed

+135
-4
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [v0.4.8] - 2025-11-11
9+
10+
### Fixed
11+
- Cascading deletion could fail in some paths due to missing ownerReferences on existing resources. Controllers now adopt pre-existing resources and ensure ownerReferences are present:
12+
- CAPTControlPlane main WorkspaceTemplateApply
13+
- CAPTControlPlane kubeconfig WorkspaceTemplateApply
14+
- CAPTCluster VPC WorkspaceTemplateApply
15+
- EC2 Spot Service-Linked Role check/create WorkspaceTemplateApply
16+
- Adoption logic is idempotent: missing ownerReferences are repaired on the next reconciliation loop.
17+
18+
### Notes
19+
- Helm chart version is unchanged in this release.
20+
821
## [v0.4.7] - 2025-11-11
922

1023
### Changed

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
VERSION = 0.4.7
1+
VERSION = 0.4.8

internal/controller/captcluster/vpc.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,20 @@ func (r *Reconciler) getOrCreateWorkspaceTemplateApply(ctx context.Context, capt
163163
return nil, err
164164
}
165165

166+
// Adopt: ensure owner reference to CAPTCluster is present on existing resource
167+
{
168+
hasOwner := false
169+
for _, ref := range latest.OwnerReferences {
170+
if ref.APIVersion == infrastructurev1beta1.GroupVersion.String() && ref.Kind == "CAPTCluster" && ref.Name == captCluster.Name {
171+
hasOwner = true
172+
break
173+
}
174+
}
175+
if !hasOwner {
176+
_ = controllerutil.SetControllerReference(captCluster, latest, r.Scheme)
177+
}
178+
}
179+
166180
// Desired spec based on current CAPTCluster
167181
desiredSpec := infrastructurev1beta1.WorkspaceTemplateApplySpec{
168182
TemplateRef: *captCluster.Spec.VPCTemplateRef,

internal/controller/controlplane/controller.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ const (
4141
func (r *Reconciler) createKubeconfigWorkspaceTemplateApply(ctx context.Context, controlPlane *controlplanev1beta1.CAPTControlPlane, cluster *clusterv1.Cluster, workspaceApply *infrastructurev1beta1.WorkspaceTemplateApply) error {
4242
logger := log.FromContext(ctx)
4343

44-
4544
// Get region from ControlPlaneConfig or cluster annotations
4645
var region string
4746
if controlPlane.Spec.ControlPlaneConfig != nil {
@@ -90,7 +89,7 @@ func (r *Reconciler) createKubeconfigWorkspaceTemplateApply(ctx context.Context,
9089
},
9190
Variables: map[string]string{
9291
"cluster_name": cluster.Name,
93-
"region": region,
92+
"region": region,
9493
},
9594
WriteConnectionSecretToRef: &xpv1.SecretReference{
9695
Name: fmt.Sprintf("%s-outputs-kubeconfig", cluster.Name),
@@ -122,6 +121,20 @@ func (r *Reconciler) createKubeconfigWorkspaceTemplateApply(ctx context.Context,
122121
return fmt.Errorf("failed to get kubeconfig WorkspaceTemplateApply: %v", err)
123122
}
124123
} else {
124+
// Adopt: ensure owner reference to CAPTControlPlane exists on the existing WTA
125+
{
126+
hasOwner := false
127+
for _, ref := range kubeconfigApply.OwnerReferences {
128+
if ref.APIVersion == controlplanev1beta1.GroupVersion.String() && ref.Kind == "CAPTControlPlane" && ref.Name == controlPlane.Name {
129+
hasOwner = true
130+
break
131+
}
132+
}
133+
if !hasOwner {
134+
_ = controllerutil.SetControllerReference(controlPlane, existingApply, r.Scheme)
135+
_ = r.Update(ctx, existingApply)
136+
}
137+
}
125138
// Update existing WorkspaceTemplateApply only if spec changed
126139
if !reflect.DeepEqual(existingApply.Spec, kubeconfigApply.Spec) {
127140
existingApply.Spec = kubeconfigApply.Spec

internal/controller/controlplane/service_linked_role.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,20 @@ func (r *Reconciler) reconcileSpotServiceLinkedRole(ctx context.Context, control
7272
}
7373

7474
// Check if the workspace apply is ready
75+
{
76+
// Adopt: ensure owner reference to CAPTControlPlane is present on existing check WTA
77+
hasOwner := false
78+
for _, ref := range checkWorkspaceApply.OwnerReferences {
79+
if ref.APIVersion == controlplanev1beta1.GroupVersion.String() && ref.Kind == "CAPTControlPlane" && ref.Name == controlPlane.Name {
80+
hasOwner = true
81+
break
82+
}
83+
}
84+
if !hasOwner {
85+
_ = controllerutil.SetControllerReference(controlPlane, checkWorkspaceApply, r.Scheme)
86+
_ = r.Update(ctx, checkWorkspaceApply)
87+
}
88+
}
7589
if !checkWorkspaceApply.Status.Applied {
7690
logger.Info("Waiting for Spot Role check workspace apply to be applied", "workspace", checkWorkspaceName)
7791
return nil
@@ -148,6 +162,21 @@ func (r *Reconciler) reconcileSpotServiceLinkedRole(ctx context.Context, control
148162
return nil
149163
}
150164

165+
// Adopt: ensure owner reference to CAPTControlPlane is present on existing create WTA
166+
{
167+
hasOwner := false
168+
for _, ref := range createWorkspaceApply.OwnerReferences {
169+
if ref.APIVersion == controlplanev1beta1.GroupVersion.String() && ref.Kind == "CAPTControlPlane" && ref.Name == controlPlane.Name {
170+
hasOwner = true
171+
break
172+
}
173+
}
174+
if !hasOwner {
175+
_ = controllerutil.SetControllerReference(controlPlane, createWorkspaceApply, r.Scheme)
176+
_ = r.Update(ctx, createWorkspaceApply)
177+
}
178+
}
179+
151180
// Check if the workspace apply is ready
152181
if !createWorkspaceApply.Status.Applied {
153182
logger.Info("Waiting for Spot Role create workspace apply to be applied", "workspace", createWorkspaceName)

internal/controller/controlplane/workspace.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,20 @@ func (r *Reconciler) getOrCreateWorkspaceTemplateApply(
6767
workspaceApply := &infrastructurev1beta1.WorkspaceTemplateApply{}
6868
err := r.Get(ctx, types.NamespacedName{Name: applyName, Namespace: controlPlane.Namespace}, workspaceApply)
6969
if err == nil {
70-
// Update existing WorkspaceTemplateApply
70+
// Adopt: ensure owner reference to CAPTControlPlane is present on existing resource
71+
{
72+
hasOwner := false
73+
for _, ref := range workspaceApply.OwnerReferences {
74+
if ref.APIVersion == controlplanev1beta1.GroupVersion.String() && ref.Kind == "CAPTControlPlane" && ref.Name == controlPlane.Name {
75+
hasOwner = true
76+
break
77+
}
78+
}
79+
if !hasOwner {
80+
_ = controllerutil.SetControllerReference(controlPlane, workspaceApply, r.Scheme)
81+
}
82+
}
83+
// Update existing WorkspaceTemplateApply spec
7184
workspaceApply.Spec = r.generateWorkspaceTemplateApplySpec(controlPlane)
7285
if err := r.Update(ctx, workspaceApply); err != nil {
7386
return nil, fmt.Errorf("failed to update WorkspaceTemplateApply: %v", err)

internal/controller/workspacetemplateapply_controller.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
corev1 "k8s.io/api/core/v1"
3232
apierrors "k8s.io/apimachinery/pkg/api/errors"
3333
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
34+
"k8s.io/apimachinery/pkg/runtime"
3435
"k8s.io/apimachinery/pkg/runtime/schema"
3536
"k8s.io/apimachinery/pkg/types"
3637
ctrl "sigs.k8s.io/controller-runtime"
@@ -215,13 +216,15 @@ func SetupWorkspaceTemplateApply(mgr ctrl.Manager, l logging.Logger) error {
215216
client: mgr.GetClient(),
216217
log: l,
217218
record: event.NewAPIRecorder(mgr.GetEventRecorderFor(controllerName)),
219+
scheme: mgr.GetScheme(),
218220
})
219221
}
220222

221223
type workspaceTemplateApplyReconciler struct {
222224
client client.Client
223225
log logging.Logger
224226
record event.Recorder
227+
scheme *runtime.Scheme
225228
}
226229

227230
func (r *workspaceTemplateApplyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
@@ -282,6 +285,29 @@ func (r *workspaceTemplateApplyReconciler) Reconcile(ctx context.Context, req ct
282285
return ctrl.Result{}, err
283286
}
284287

288+
// If a Workspace with the expected name already exists (e.g., from previous versions),
289+
// adopt it by setting ownerReference and updating status instead of failing creation.
290+
existing := &tfv1beta1.Workspace{}
291+
if err := r.client.Get(ctx, types.NamespacedName{
292+
Name: workspaceName,
293+
Namespace: cr.Namespace,
294+
}, existing); err == nil {
295+
// Ensure owner reference to this WorkspaceTemplateApply so GC can cascade on delete.
296+
if setErr := controllerutil.SetControllerReference(cr, existing, r.scheme); setErr == nil {
297+
_ = r.client.Update(ctx, existing)
298+
}
299+
// Update status to reflect adoption
300+
cr.Status.WorkspaceName = existing.GetName()
301+
cr.Status.Applied = true
302+
now := metav1.Now()
303+
cr.Status.LastAppliedTime = &now
304+
if uerr := r.client.Status().Update(ctx, cr); uerr != nil {
305+
return ctrl.Result{}, uerr
306+
}
307+
r.record.Event(cr, event.Normal(reasonCreatedWorkspace, "Adopted existing Workspace"))
308+
return ctrl.Result{RequeueAfter: requeueAfterStatus}, nil
309+
}
310+
285311
// Create Workspace from template
286312
workspace := &tfv1beta1.Workspace{
287313
ObjectMeta: metav1.ObjectMeta{
@@ -296,6 +322,12 @@ func (r *workspaceTemplateApplyReconciler) Reconcile(ctx context.Context, req ct
296322
workspace.Spec.WriteConnectionSecretToReference = cr.Spec.WriteConnectionSecretToRef
297323
}
298324

325+
// Set owner reference so Workspace is garbage-collected when WTA is deleted.
326+
if err := controllerutil.SetControllerReference(cr, workspace, r.scheme); err != nil {
327+
log.Debug("Failed to set owner reference on Workspace", "error", err)
328+
return ctrl.Result{}, err
329+
}
330+
299331
if err := r.client.Create(ctx, workspace); err != nil {
300332
log.Debug(errCreateWorkspace, "error", err)
301333
return ctrl.Result{}, err
@@ -393,6 +425,13 @@ func (r *workspaceTemplateApplyReconciler) reconcileWorkspaceStatus(ctx context.
393425
return ctrl.Result{}, err
394426
}
395427

428+
// Ensure owner reference to WTA is present on Workspace (adopt if missing).
429+
if !hasOwnerReference(workspace.GetOwnerReferences(), cr.APIVersion, cr.Kind, cr.Name, string(cr.UID)) {
430+
if err := controllerutil.SetControllerReference(cr, workspace, r.scheme); err == nil {
431+
_ = r.client.Update(ctx, workspace)
432+
}
433+
}
434+
396435
// Copy conditions from workspace to WorkspaceTemplateApply
397436
cr.Status.Conditions = workspace.Status.Conditions
398437

@@ -419,3 +458,13 @@ func (r *workspaceTemplateApplyReconciler) reconcileWorkspaceStatus(ctx context.
419458
r.record.Event(cr, event.Normal(reasonWorkspaceReady, "Workspace is synced and ready"))
420459
return ctrl.Result{}, nil
421460
}
461+
462+
// hasOwnerReference returns true if an owner reference matching the given attributes exists.
463+
func hasOwnerReference(refs []metav1.OwnerReference, apiVersion, kind, name, uid string) bool {
464+
for _, ref := range refs {
465+
if ref.APIVersion == apiVersion && ref.Kind == kind && ref.Name == name && string(ref.UID) == uid {
466+
return true
467+
}
468+
}
469+
return false
470+
}

0 commit comments

Comments
 (0)