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

heathcliff26 / kube-upgrade / 21561672726

01 Feb 2026 10:59AM UTC coverage: 73.866% (-0.07%) from 73.935%
21561672726

Pull #229

github

web-flow
Merge 815b1f125 into 622108658
Pull Request #229: fix(deps): update sigs.k8s.io/controller-runtime to v0.23.1

1 of 2 new or added lines in 2 files covered. (50.0%)

1 existing line in 1 file now uncovered.

1173 of 1588 relevant lines covered (73.87%)

11.24 hits per line

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

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

3
import (
4
        "context"
5
        "fmt"
6
        "log/slog"
7
        "time"
8

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/client-go/rest"
15
        ctrl "sigs.k8s.io/controller-runtime"
16
        "sigs.k8s.io/controller-runtime/pkg/cache"
17
        "sigs.k8s.io/controller-runtime/pkg/client"
18
        "sigs.k8s.io/controller-runtime/pkg/healthz"
19
        "sigs.k8s.io/controller-runtime/pkg/manager"
20
        "sigs.k8s.io/controller-runtime/pkg/manager/signals"
21
)
22

23
const (
24
        defaultUpgradedImage = "ghcr.io/heathcliff26/kube-upgraded"
25
        upgradedImageEnv     = "UPGRADED_IMAGE"
26
        upgradedTagEnv       = "UPGRADED_TAG"
27
)
28

29
type controller struct {
30
        client.Client
31
        manager       manager.Manager
32
        namespace     string
33
        upgradedImage string
34
}
35

36
// Run make generate when changing these comments
37
// +kubebuilder:rbac:groups=kubeupgrade.heathcliff.eu,resources=kubeupgradeplans,verbs=get;list;watch;create;update;patch;delete
38
// +kubebuilder:rbac:groups=kubeupgrade.heathcliff.eu,resources=kubeupgradeplans/status,verbs=get;update;patch
39
// +kubebuilder:rbac:groups="",resources=nodes,verbs=list;watch;update
40
// +kubebuilder:rbac:groups="",namespace=kube-upgrade,resources=events,verbs=create;patch
41
// +kubebuilder:rbac:groups="coordination.k8s.io",namespace=kube-upgrade,resources=leases,verbs=create;get;update
42
// +kubebuilder:rbac:groups="apps",namespace=kube-upgrade,resources=daemonsets,verbs=list;watch;create;update;delete
43
// +kubebuilder:rbac:groups="",namespace=kube-upgrade,resources=configmaps,verbs=list;watch;create;update;delete
44

45
func NewController(name string) (*controller, error) {
1✔
46
        config, err := rest.InClusterConfig()
1✔
47
        if err != nil {
2✔
48
                return nil, err
1✔
49
        }
1✔
50

51
        ns, err := GetNamespace()
×
52
        if err != nil {
×
53
                return nil, err
×
54
        }
×
55

56
        scheme, err := newScheme()
×
57
        if err != nil {
×
58
                return nil, err
×
59
        }
×
60

61
        mgr, err := ctrl.NewManager(config, manager.Options{
×
62
                Scheme:                        scheme,
×
63
                LeaderElection:                true,
×
64
                LeaderElectionNamespace:       ns,
×
65
                LeaderElectionID:              name,
×
66
                LeaderElectionReleaseOnCancel: true,
×
67
                LeaseDuration:                 Pointer(time.Minute),
×
68
                RenewDeadline:                 Pointer(10 * time.Second),
×
69
                RetryPeriod:                   Pointer(5 * time.Second),
×
70
                HealthProbeBindAddress:        ":9090",
×
71
                Cache: cache.Options{
×
72
                        DefaultNamespaces: map[string]cache.Config{ns: {}},
×
73
                },
×
74
        })
×
75
        if err != nil {
×
76
                return nil, err
×
77
        }
×
78
        err = mgr.AddHealthzCheck("healthz", healthz.Ping)
×
79
        if err != nil {
×
80
                return nil, err
×
81
        }
×
82
        err = mgr.AddReadyzCheck("readyz", healthz.Ping)
×
83
        if err != nil {
×
84
                return nil, err
×
85
        }
×
86

87
        return &controller{
×
88
                Client:        mgr.GetClient(),
×
89
                manager:       mgr,
×
90
                namespace:     ns,
×
91
                upgradedImage: GetUpgradedImage(),
×
92
        }, nil
×
93
}
94

95
func (c *controller) Run() error {
×
96
        err := ctrl.NewControllerManagedBy(c.manager).
×
97
                For(&api.KubeUpgradePlan{}).
×
98
                Owns(&appv1.DaemonSet{}).
×
99
                Owns(&corev1.ConfigMap{}).
×
100
                Complete(c)
×
101
        if err != nil {
×
102
                return err
×
103
        }
×
104

NEW
105
        err = ctrl.NewWebhookManagedBy(c.manager, &api.KubeUpgradePlan{}).
×
106
                WithDefaulter(&planMutatingHook{}).
×
107
                WithValidator(&planValidatingHook{
×
108
                        Client: c.Client,
×
109
                }).
×
110
                Complete()
×
111
        if err != nil {
×
112
                return err
×
113
        }
×
114

115
        return c.manager.Start(signals.SetupSignalHandler())
×
116
}
117

118
func (c *controller) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
×
119
        logger := slog.With("plan", req.Name)
×
120

×
121
        var plan api.KubeUpgradePlan
×
122
        err := c.Get(ctx, req.NamespacedName, &plan)
×
123
        if err != nil {
×
124
                logger.Error("Failed to get Plan", "err", err)
×
125
                return ctrl.Result{}, err
×
126
        }
×
127

128
        err = c.reconcile(ctx, &plan, logger)
×
129
        if err != nil {
×
130
                return ctrl.Result{}, err
×
131
        }
×
132

133
        err = c.Status().Update(ctx, &plan)
×
134
        if err != nil {
×
135
                logger.Error("Failed to update plan status", "err", err)
×
136
                return ctrl.Result{}, err
×
137
        }
×
138

139
        return ctrl.Result{
×
140
                Requeue:      plan.Status.Summary != api.PlanStatusComplete,
×
141
                RequeueAfter: time.Minute,
×
142
        }, nil
×
143
}
144

145
func (c *controller) reconcile(ctx context.Context, plan *api.KubeUpgradePlan, logger *slog.Logger) error {
15✔
146
        if plan.Status.Groups == nil {
24✔
147
                plan.Status.Groups = make(map[string]string, len(plan.Spec.Groups))
9✔
148
        }
9✔
149

150
        cmList := &corev1.ConfigMapList{}
15✔
151
        err := c.List(ctx, cmList, client.InNamespace(c.namespace), client.MatchingLabels{
15✔
152
                constants.LabelPlanName: plan.Name,
15✔
153
        })
15✔
154
        if err != nil {
15✔
155
                logger.Error("Failed to fetch upgraded ConfigMaps", "err", err)
×
156
                return err
×
157
        }
×
158

159
        dsList := &appv1.DaemonSetList{}
15✔
160
        err = c.List(ctx, dsList, client.InNamespace(c.namespace), client.MatchingLabels{
15✔
161
                constants.LabelPlanName: plan.Name,
15✔
162
        })
15✔
163
        if err != nil {
15✔
164
                logger.Error("Failed to fetch upgraded DaemonSets", "err", err)
×
165
                return err
×
166
        }
×
167

168
        daemons := make(map[string]*appv1.DaemonSet, len(plan.Spec.Groups))
15✔
169
        for i := range dsList.Items {
23✔
170
                daemon := &dsList.Items[i]
8✔
171
                group := daemon.Labels[constants.LabelNodeGroup]
8✔
172
                if _, ok := plan.Spec.Groups[group]; ok {
14✔
173
                        daemons[group] = daemon
6✔
174
                } else {
8✔
175
                        err = c.Delete(ctx, daemon)
2✔
176
                        if err != nil {
2✔
177
                                return fmt.Errorf("failed to delete DaemonSet %s: %v", daemon.Name, err)
×
178
                        }
×
179
                        logger.Info("Deleted obsolete DaemonSet", "name", daemon.Name)
2✔
180
                }
181
        }
182

183
        cms := make(map[string]*corev1.ConfigMap, len(plan.Spec.Groups))
15✔
184
        for i := range cmList.Items {
23✔
185
                cm := &cmList.Items[i]
8✔
186
                group := cm.Labels[constants.LabelNodeGroup]
8✔
187
                if _, ok := plan.Spec.Groups[group]; ok {
14✔
188
                        cms[group] = cm
6✔
189
                } else {
8✔
190
                        err = c.Delete(ctx, cm)
2✔
191
                        if err != nil {
2✔
192
                                return fmt.Errorf("failed to delete ConfigMap %s: %v", cm.Name, err)
×
193
                        }
×
194
                        logger.Info("Deleted obsolete ConfigMap", "name", cm.Name)
2✔
195
                }
196
        }
197

198
        nodesToUpdate := make(map[string][]corev1.Node, len(plan.Spec.Groups))
15✔
199
        newGroupStatus := make(map[string]string, len(plan.Spec.Groups))
15✔
200

15✔
201
        for name, cfg := range plan.Spec.Groups {
49✔
202
                logger := logger.With("group", name)
34✔
203

34✔
204
                err = c.reconcileUpgradedConfigMap(ctx, plan, logger, cms[name], name)
34✔
205
                if err != nil {
34✔
206
                        return fmt.Errorf("failed to reconcile ConfigMap for group %s: %v", name, err)
×
207
                }
×
208

209
                err = c.reconcileUpgradedDaemonSet(ctx, plan, logger, daemons[name], name, cfg)
34✔
210
                if err != nil {
34✔
211
                        return fmt.Errorf("failed to reconcile DaemonSet for group %s: %v", name, err)
×
212
                }
×
213

214
                nodeList := &corev1.NodeList{}
34✔
215
                err = c.List(ctx, nodeList, client.MatchingLabels(cfg.Labels))
34✔
216
                if err != nil {
34✔
217
                        logger.Error("Failed to get nodes for group", "err", err)
×
218
                        return err
×
219
                }
×
220

221
                status, update, nodes, err := c.reconcileNodes(plan.Spec.KubernetesVersion, plan.Spec.AllowDowngrade, nodeList.Items)
34✔
222
                if err != nil {
34✔
223
                        logger.Error("Failed to reconcile nodes for group", "err", err)
×
224
                        return err
×
225
                }
×
226

227
                newGroupStatus[name] = status
34✔
228

34✔
229
                if update {
58✔
230
                        nodesToUpdate[name] = nodes
24✔
231
                } else if plan.Status.Groups[name] != newGroupStatus[name] {
40✔
232
                        logger.Info("Group changed status", "status", newGroupStatus[name])
6✔
233
                }
6✔
234
        }
235

236
        for name, nodes := range nodesToUpdate {
39✔
237
                logger := logger.With("group", name)
24✔
238

24✔
239
                if groupWaitForDependency(plan.Spec.Groups[name].DependsOn, newGroupStatus) {
30✔
240
                        logger.Info("Group is waiting on dependencies")
6✔
241
                        newGroupStatus[name] = api.PlanStatusWaiting
6✔
242
                        continue
6✔
243
                } else if plan.Status.Groups[name] != newGroupStatus[name] {
36✔
244
                        logger.Info("Group changed status", "status", newGroupStatus[name])
18✔
245
                }
18✔
246

247
                for _, node := range nodes {
36✔
248
                        logger.Debug("Updating node annotations", "node", node.Name)
18✔
249
                        err = c.Update(ctx, &node)
18✔
250
                        if err != nil {
18✔
251
                                return fmt.Errorf("failed to update node %s: %v", node.GetName(), err)
×
252
                        }
×
253
                }
254
        }
255

256
        plan.Status.Groups = newGroupStatus
15✔
257
        plan.Status.Summary = createStatusSummary(plan.Status.Groups)
15✔
258

15✔
259
        return nil
15✔
260
}
261

262
func (c *controller) reconcileNodes(kubeVersion string, downgrade bool, nodes []corev1.Node) (string, bool, []corev1.Node, error) {
36✔
263
        if len(nodes) == 0 {
36✔
264
                return api.PlanStatusUnknown, false, nil, nil
×
265
        }
×
266

267
        completed := 0
36✔
268
        needUpdate := false
36✔
269
        errorNodes := make([]string, 0)
36✔
270

36✔
271
        for i := range nodes {
72✔
272
                if nodes[i].Annotations == nil {
59✔
273
                        nodes[i].Annotations = make(map[string]string)
23✔
274
                }
23✔
275

276
                if !downgrade && semver.Compare(kubeVersion, nodes[i].Status.NodeInfo.KubeletVersion) < 0 {
37✔
277
                        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✔
278
                }
1✔
279

280
                if nodes[i].Annotations[constants.NodeKubernetesVersion] == kubeVersion {
45✔
281
                        switch nodes[i].Annotations[constants.NodeUpgradeStatus] {
10✔
282
                        case constants.NodeUpgradeStatusCompleted:
9✔
283
                                completed++
9✔
284
                        case constants.NodeUpgradeStatusError:
1✔
285
                                errorNodes = append(errorNodes, nodes[i].GetName())
1✔
286
                        }
287
                        continue
10✔
288
                }
289

290
                nodes[i].Annotations[constants.NodeKubernetesVersion] = kubeVersion
25✔
291
                nodes[i].Annotations[constants.NodeUpgradeStatus] = constants.NodeUpgradeStatusPending
25✔
292

25✔
293
                needUpdate = true
25✔
294
        }
295

296
        var status string
35✔
297
        if len(errorNodes) > 0 {
36✔
298
                status = fmt.Sprintf("%s: The nodes %v are reporting errors", api.PlanStatusError, errorNodes)
1✔
299
        } else if len(nodes) == completed {
44✔
300
                status = api.PlanStatusComplete
9✔
301
        } else {
34✔
302
                status = fmt.Sprintf("%s: %d/%d nodes upgraded", api.PlanStatusProgressing, completed, len(nodes))
25✔
303
        }
25✔
304
        return status, needUpdate, nodes, nil
35✔
305
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc