• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

heathcliff26 / kube-upgrade / 19368906375

14 Nov 2025 03:14PM UTC coverage: 72.538% (+0.5%) from 72.083%
19368906375

push

github

heathcliff26
controller: Use owner references instead of finalizers

Ensure created resources are owned by the KubeUpgradePlan.
This allows Kubernetes to automatically clean up resources when the plan is
deleted, removing the need for finalizers and related cleanup logic.

Signed-off-by: Heathcliff <heathcliff@heathcliff.eu>

13 of 18 new or added lines in 2 files covered. (72.22%)

1009 of 1391 relevant lines covered (72.54%)

12.37 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

52.21
/pkg/upgrade-controller/controller/controller.go
1
package controller
2

3
import (
4
        "context"
5
        "fmt"
6
        "time"
7

8
        "github.com/go-logr/logr"
9
        api "github.com/heathcliff26/kube-upgrade/pkg/apis/kubeupgrade/v1alpha3"
10
        "github.com/heathcliff26/kube-upgrade/pkg/constants"
11
        "golang.org/x/mod/semver"
12
        appv1 "k8s.io/api/apps/v1"
13
        corev1 "k8s.io/api/core/v1"
14
        "k8s.io/apimachinery/pkg/runtime"
15
        clientgoscheme "k8s.io/client-go/kubernetes/scheme"
16
        "k8s.io/client-go/rest"
17
        "k8s.io/klog/v2"
18
        ctrl "sigs.k8s.io/controller-runtime"
19
        "sigs.k8s.io/controller-runtime/pkg/cache"
20
        "sigs.k8s.io/controller-runtime/pkg/client"
21
        "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
22
        "sigs.k8s.io/controller-runtime/pkg/healthz"
23
        "sigs.k8s.io/controller-runtime/pkg/manager"
24
        "sigs.k8s.io/controller-runtime/pkg/manager/signals"
25
)
26

27
const (
28
        defaultUpgradedImage = "ghcr.io/heathcliff26/kube-upgraded"
29
        upgradedImageEnv     = "UPGRADED_IMAGE"
30
        upgradedTagEnv       = "UPGRADED_TAG"
31
)
32

33
func init() {
2✔
34
        ctrl.SetLogger(klog.NewKlogr())
2✔
35
}
2✔
36

37
type controller struct {
38
        client.Client
39
        manager       manager.Manager
40
        namespace     string
41
        upgradedImage string
42
}
43

44
// Run make generate when changing these comments
45
// +kubebuilder:rbac:groups=kubeupgrade.heathcliff.eu,resources=kubeupgradeplans,verbs=get;list;watch;create;update;patch;delete
46
// +kubebuilder:rbac:groups=kubeupgrade.heathcliff.eu,resources=kubeupgradeplans/status,verbs=get;update;patch
47
// +kubebuilder:rbac:groups="",resources=nodes,verbs=list;update
48
// +kubebuilder:rbac:groups="",namespace=kube-upgrade,resources=events,verbs=create;patch
49
// +kubebuilder:rbac:groups="coordination.k8s.io",namespace=kube-upgrade,resources=leases,verbs=create;get;update
50
// +kubebuilder:rbac:groups="apps",namespace=kube-upgrade,resources=daemonsets,verbs=list;watch;create;update;delete
51
// +kubebuilder:rbac:groups="",namespace=kube-upgrade,resources=configmaps,verbs=list;watch;create;update;delete
52

53
func NewController(name string) (*controller, error) {
1✔
54
        config, err := rest.InClusterConfig()
1✔
55
        if err != nil {
2✔
56
                return nil, err
1✔
57
        }
1✔
58

59
        ns, err := GetNamespace()
×
60
        if err != nil {
×
61
                return nil, err
×
62
        }
×
63

64
        scheme := runtime.NewScheme()
×
65
        err = api.AddToScheme(scheme)
×
66
        if err != nil {
×
67
                return nil, err
×
68
        }
×
69
        err = clientgoscheme.AddToScheme(scheme)
×
70
        if err != nil {
×
71
                return nil, err
×
72
        }
×
73

74
        mgr, err := ctrl.NewManager(config, manager.Options{
×
75
                Scheme:                        scheme,
×
76
                LeaderElection:                true,
×
77
                LeaderElectionNamespace:       ns,
×
78
                LeaderElectionID:              name,
×
79
                LeaderElectionReleaseOnCancel: true,
×
80
                LeaseDuration:                 Pointer(time.Minute),
×
81
                RenewDeadline:                 Pointer(10 * time.Second),
×
82
                RetryPeriod:                   Pointer(5 * time.Second),
×
83
                HealthProbeBindAddress:        ":9090",
×
84
                Cache: cache.Options{
×
85
                        DefaultNamespaces: map[string]cache.Config{ns: {}},
×
86
                },
×
87
        })
×
88
        if err != nil {
×
89
                return nil, err
×
90
        }
×
91
        err = mgr.AddHealthzCheck("healthz", healthz.Ping)
×
92
        if err != nil {
×
93
                return nil, err
×
94
        }
×
95
        err = mgr.AddReadyzCheck("readyz", healthz.Ping)
×
96
        if err != nil {
×
97
                return nil, err
×
98
        }
×
99

100
        return &controller{
×
101
                Client:        mgr.GetClient(),
×
102
                manager:       mgr,
×
103
                namespace:     ns,
×
104
                upgradedImage: GetUpgradedImage(),
×
105
        }, nil
×
106
}
107

108
func (c *controller) Run() error {
×
109
        err := ctrl.NewControllerManagedBy(c.manager).
×
110
                For(&api.KubeUpgradePlan{}).
×
111
                Owns(&appv1.DaemonSet{}).
×
112
                Owns(&corev1.ConfigMap{}).
×
113
                Complete(c)
×
114
        if err != nil {
×
115
                return err
×
116
        }
×
117

118
        err = ctrl.NewWebhookManagedBy(c.manager).
×
119
                For(&api.KubeUpgradePlan{}).
×
120
                WithDefaulter(&planMutatingHook{}).
×
121
                WithValidator(&planValidatingHook{}).
×
122
                Complete()
×
123
        if err != nil {
×
124
                return err
×
125
        }
×
126

127
        return c.manager.Start(signals.SetupSignalHandler())
×
128
}
129

130
func (c *controller) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
×
131
        logger := klog.LoggerWithValues(klog.NewKlogr(), "plan", req.Name)
×
132

×
133
        var plan api.KubeUpgradePlan
×
134
        err := c.Get(ctx, req.NamespacedName, &plan)
×
135
        if err != nil {
×
136
                logger.Error(err, "Failed to get Plan")
×
137
                return ctrl.Result{}, err
×
138
        }
×
139

140
        err = c.reconcile(ctx, &plan, logger)
×
141
        if err != nil {
×
142
                return ctrl.Result{}, err
×
143
        }
×
144

145
        err = c.Status().Update(ctx, &plan)
×
146
        if err != nil {
×
147
                logger.Error(err, "Failed to update plan status")
×
148
                return ctrl.Result{}, err
×
149
        }
×
150

151
        return ctrl.Result{
×
152
                Requeue:      plan.Status.Summary != api.PlanStatusComplete,
×
153
                RequeueAfter: time.Minute,
×
154
        }, nil
×
155
}
156

157
func (c *controller) reconcile(ctx context.Context, plan *api.KubeUpgradePlan, logger logr.Logger) error {
17✔
158
        if plan.Status.Groups == nil {
27✔
159
                plan.Status.Groups = make(map[string]string, len(plan.Spec.Groups))
10✔
160
        }
10✔
161

162
        // Migration from v0.6.0: Remove the finalizer as it is not needed
163
        // TODO: Remove in future release
164
        if controllerutil.RemoveFinalizer(plan, constants.Finalizer) {
18✔
165
                err := c.Update(ctx, plan)
1✔
166
                if err != nil {
1✔
NEW
167
                        return fmt.Errorf("failed to remove finalizer from plan %s: %v", plan.Name, err)
×
168
                }
×
169
        }
170

171
        cmList := &corev1.ConfigMapList{}
17✔
172
        err := c.List(ctx, cmList, client.InNamespace(c.namespace), client.MatchingLabels{
17✔
173
                constants.LabelPlanName: plan.Name,
17✔
174
        })
17✔
175
        if err != nil {
17✔
176
                logger.WithValues("plan", plan.Name).Error(err, "Failed to fetch upgraded ConfigMaps")
×
177
                return err
×
178
        }
×
179

180
        dsList := &appv1.DaemonSetList{}
17✔
181
        err = c.List(ctx, dsList, client.InNamespace(c.namespace), client.MatchingLabels{
17✔
182
                constants.LabelPlanName: plan.Name,
17✔
183
        })
17✔
184
        if err != nil {
17✔
185
                logger.WithValues("plan", plan.Name).Error(err, "Failed to fetch upgraded DaemonSets")
×
186
                return err
×
187
        }
×
188

189
        daemons := make(map[string]*appv1.DaemonSet, len(plan.Spec.Groups))
17✔
190
        for i := range dsList.Items {
25✔
191
                daemon := &dsList.Items[i]
8✔
192
                group := daemon.Labels[constants.LabelNodeGroup]
8✔
193
                if _, ok := plan.Spec.Groups[group]; ok {
14✔
194
                        daemons[group] = daemon
6✔
195
                } else {
8✔
196
                        err = c.Delete(ctx, daemon)
2✔
197
                        if err != nil {
2✔
198
                                return fmt.Errorf("failed to delete DaemonSet %s: %v", daemon.Name, err)
×
199
                        }
×
200
                        logger.WithValues("name", daemon.Name).Info("Deleted obsolete DaemonSet")
2✔
201
                }
202
        }
203

204
        cms := make(map[string]*corev1.ConfigMap, len(plan.Spec.Groups))
17✔
205
        for i := range cmList.Items {
25✔
206
                cm := &cmList.Items[i]
8✔
207
                group := cm.Labels[constants.LabelNodeGroup]
8✔
208
                if _, ok := plan.Spec.Groups[group]; ok {
14✔
209
                        cms[group] = cm
6✔
210
                } else {
8✔
211
                        err = c.Delete(ctx, cm)
2✔
212
                        if err != nil {
2✔
213
                                return fmt.Errorf("failed to delete ConfigMap %s: %v", cm.Name, err)
×
214
                        }
×
215
                        logger.WithValues("name", cm.Name).Info("Deleted obsolete ConfigMap")
2✔
216
                }
217
        }
218

219
        nodesToUpdate := make(map[string][]corev1.Node, len(plan.Spec.Groups))
17✔
220
        newGroupStatus := make(map[string]string, len(plan.Spec.Groups))
17✔
221

17✔
222
        for name, cfg := range plan.Spec.Groups {
54✔
223
                err = c.reconcileUpgradedConfigMap(ctx, plan, logger, cms[name], name)
37✔
224
                if err != nil {
37✔
225
                        return fmt.Errorf("failed to reconcile ConfigMap for group %s: %v", name, err)
×
226
                }
×
227

228
                err = c.reconcileUpgradedDaemonSet(ctx, plan, logger, daemons[name], name, cfg)
37✔
229
                if err != nil {
37✔
230
                        return fmt.Errorf("failed to reconcile DaemonSet for group %s: %v", name, err)
×
231
                }
×
232

233
                nodeList := &corev1.NodeList{}
37✔
234
                err = c.List(ctx, nodeList, client.MatchingLabels(cfg.Labels))
37✔
235
                if err != nil {
37✔
236
                        logger.WithValues("group", name).Error(err, "Failed to get nodes for group")
×
237
                        return err
×
238
                }
×
239

240
                status, update, nodes, err := c.reconcileNodes(plan.Spec.KubernetesVersion, plan.Spec.AllowDowngrade, nodeList.Items)
37✔
241
                if err != nil {
37✔
242
                        logger.WithValues("group", name).Error(err, "Failed to reconcile nodes for group")
×
243
                        return err
×
244
                }
×
245

246
                newGroupStatus[name] = status
37✔
247

37✔
248
                if update {
60✔
249
                        nodesToUpdate[name] = nodes
23✔
250
                } else if plan.Status.Groups[name] != newGroupStatus[name] {
47✔
251
                        logger.WithValues("group", name, "status", newGroupStatus[name]).Info("Group changed status")
10✔
252
                }
10✔
253
        }
254

255
        for name, nodes := range nodesToUpdate {
40✔
256
                if groupWaitForDependency(plan.Spec.Groups[name].DependsOn, newGroupStatus) {
29✔
257
                        logger.WithValues("group", name).Info("Group is waiting on dependencies")
6✔
258
                        newGroupStatus[name] = api.PlanStatusWaiting
6✔
259
                        continue
6✔
260
                } else if plan.Status.Groups[name] != newGroupStatus[name] {
32✔
261
                        logger.WithValues("group", name, "status", newGroupStatus[name]).Info("Group changed status")
15✔
262
                }
15✔
263

264
                for _, node := range nodes {
34✔
265
                        err = c.Update(ctx, &node)
17✔
266
                        if err != nil {
17✔
267
                                return fmt.Errorf("failed to update node %s: %v", node.GetName(), err)
×
268
                        }
×
269
                }
270
        }
271

272
        plan.Status.Groups = newGroupStatus
17✔
273
        plan.Status.Summary = createStatusSummary(plan.Status.Groups)
17✔
274

17✔
275
        return nil
17✔
276
}
277

278
func (c *controller) reconcileNodes(kubeVersion string, downgrade bool, nodes []corev1.Node) (string, bool, []corev1.Node, error) {
39✔
279
        if len(nodes) == 0 {
43✔
280
                return api.PlanStatusUnknown, false, nil, nil
4✔
281
        }
4✔
282

283
        completed := 0
35✔
284
        needUpdate := false
35✔
285
        errorNodes := make([]string, 0)
35✔
286

35✔
287
        for i := range nodes {
70✔
288
                if nodes[i].Annotations == nil {
55✔
289
                        nodes[i].Annotations = make(map[string]string)
20✔
290
                }
20✔
291

292
                // Step to cleanup after migration to v0.6.0
293
                // TODO: Remove in v0.7.0
294
                if deleteConfigAnnotations(nodes[i].Annotations) {
37✔
295
                        needUpdate = true
2✔
296
                }
2✔
297

298
                if !downgrade && semver.Compare(kubeVersion, nodes[i].Status.NodeInfo.KubeletVersion) < 0 {
36✔
299
                        return api.PlanStatusError, false, nil, fmt.Errorf("node %s version %s is newer than %s, but downgrade is disabled", nodes[i].GetName(), nodes[i].Status.NodeInfo.KubeletVersion, kubeVersion)
1✔
300
                }
1✔
301

302
                if nodes[i].Annotations[constants.NodeKubernetesVersion] == kubeVersion {
46✔
303
                        switch nodes[i].Annotations[constants.NodeUpgradeStatus] {
12✔
304
                        case constants.NodeUpgradeStatusCompleted:
11✔
305
                                completed++
11✔
306
                        case constants.NodeUpgradeStatusError:
1✔
307
                                errorNodes = append(errorNodes, nodes[i].GetName())
1✔
308
                        }
309
                        continue
12✔
310
                }
311

312
                nodes[i].Annotations[constants.NodeKubernetesVersion] = kubeVersion
22✔
313
                nodes[i].Annotations[constants.NodeUpgradeStatus] = constants.NodeUpgradeStatusPending
22✔
314

22✔
315
                needUpdate = true
22✔
316
        }
317

318
        var status string
34✔
319
        if len(errorNodes) > 0 {
35✔
320
                status = fmt.Sprintf("%s: The nodes %v are reporting errors", api.PlanStatusError, errorNodes)
1✔
321
        } else if len(nodes) == completed {
45✔
322
                status = api.PlanStatusComplete
11✔
323
        } else {
33✔
324
                status = fmt.Sprintf("%s: %d/%d nodes upgraded", api.PlanStatusProgressing, completed, len(nodes))
22✔
325
        }
22✔
326
        return status, needUpdate, nodes, nil
34✔
327
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc