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

heathcliff26 / kube-upgrade / 19393426312

15 Nov 2025 05:51PM UTC coverage: 72.307% (+0.2%) from 72.155%
19393426312

push

github

heathcliff26
upgrade-controller: Use validating webhook to ensure only a single plan exists

There should only ever be a single KubeUpgradePlan in the cluster at any time.
To enforce this, add a validating webhook that checks for existing plans
when a new plan is created, and rejects the creation if one already exists.

Closes: #187

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

18 of 28 new or added lines in 3 files covered. (64.29%)

1094 of 1513 relevant lines covered (72.31%)

11.7 hits per line

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

93.59
/pkg/upgrade-controller/controller/utils.go
1
package controller
2

3
import (
4
        "errors"
5
        "fmt"
6
        "log/slog"
7
        "os"
8
        "strings"
9

10
        api "github.com/heathcliff26/kube-upgrade/pkg/apis/kubeupgrade/v1alpha3"
11
        "github.com/heathcliff26/kube-upgrade/pkg/version"
12
        "k8s.io/apimachinery/pkg/runtime"
13
        clientgoscheme "k8s.io/client-go/kubernetes/scheme"
14
)
15

16
var serviceAccountNamespaceFile = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"
17

18
const namespaceKubeUpgrade = "kube-upgrade"
19

20
// Read the namespace from the inserted serviceaccount file. Fallback to default if the file does not exist.
21
func GetNamespace() (string, error) {
3✔
22
        data, err := os.ReadFile(serviceAccountNamespaceFile)
3✔
23
        if err != nil {
4✔
24
                if !errors.Is(err, os.ErrNotExist) {
1✔
25
                        return "", NewErrorGetNamespace(serviceAccountNamespaceFile, err)
×
26
                } else {
1✔
27
                        return namespaceKubeUpgrade, nil
1✔
28
                }
1✔
29
        }
30

31
        ns := strings.TrimSpace(string(data))
2✔
32
        if len(ns) == 0 {
3✔
33
                return "", NewErrorGetNamespace(serviceAccountNamespaceFile, fmt.Errorf("file was empty"))
1✔
34
        }
1✔
35
        return ns, nil
1✔
36
}
37

38
// Return a pointer to the variable value
39
func Pointer[T any](v T) *T {
45✔
40
        return &v
45✔
41
}
45✔
42

43
// Check if the given group needs to wait on another one
44
func groupWaitForDependency(deps []string, status map[string]string) bool {
28✔
45
        for _, d := range deps {
44✔
46
                if status[d] != api.PlanStatusComplete {
23✔
47
                        return true
7✔
48
                }
7✔
49
        }
50
        return false
21✔
51
}
52

53
// Return the status summary from the given input
54
func createStatusSummary(status map[string]string) string {
22✔
55
        if len(status) == 0 {
23✔
56
                return api.PlanStatusUnknown
1✔
57
        }
1✔
58
        waiting := false
21✔
59
        unknown := false
21✔
60
        progressing := make([]string, 0, len(status))
21✔
61
        errorGroups := make([]string, 0, len(status))
21✔
62

21✔
63
        for group, s := range status {
71✔
64
                switch {
50✔
65
                case s == api.PlanStatusComplete:
18✔
66
                case strings.HasPrefix(s, api.PlanStatusProgressing):
22✔
67
                        progressing = append(progressing, group)
22✔
68
                case s == api.PlanStatusWaiting:
7✔
69
                        waiting = true
7✔
70
                case strings.HasPrefix(s, api.PlanStatusError):
2✔
71
                        errorGroups = append(errorGroups, group)
2✔
72
                default:
1✔
73
                        unknown = true
1✔
74
                }
75
        }
76

77
        if unknown {
22✔
78
                return api.PlanStatusUnknown
1✔
79
        } else if len(errorGroups) > 0 {
23✔
80
                return fmt.Sprintf("%s: Some groups encountered errors %v", api.PlanStatusError, errorGroups)
2✔
81
        } else if len(progressing) > 0 {
34✔
82
                return fmt.Sprintf("%s: Upgrading groups %v", api.PlanStatusProgressing, progressing)
14✔
83
        } else if waiting {
19✔
84
                return api.PlanStatusWaiting
1✔
85
        } else {
4✔
86
                return api.PlanStatusComplete
3✔
87
        }
3✔
88
}
89

90
// Return the upgraded image to use based on environment variables
91
func GetUpgradedImage() string {
4✔
92
        logger := slog.With("env", upgradedImageEnv)
4✔
93

4✔
94
        image := os.Getenv(upgradedImageEnv)
4✔
95
        tag := os.Getenv(upgradedTagEnv)
4✔
96
        if image == "" {
6✔
97
                logger.Info("Upgraded image is not set, falling back to default", "image", defaultUpgradedImage)
2✔
98
                image = defaultUpgradedImage
2✔
99
        }
2✔
100
        if tag == "" {
6✔
101
                logger.Info("Upgraded image tag is not set, falling back to default", "tag", version.Version())
2✔
102
                tag = version.Version()
2✔
103
        }
2✔
104
        return fmt.Sprintf("%s:%s", image, tag)
4✔
105
}
106

107
// Return a new scheme with already registered standard types and kube-upgrade types
108
func newScheme() (*runtime.Scheme, error) {
17✔
109
        scheme := runtime.NewScheme()
17✔
110
        err := api.AddToScheme(scheme)
17✔
111
        if err != nil {
17✔
NEW
112
                return nil, err
×
NEW
113
        }
×
114
        err = clientgoscheme.AddToScheme(scheme)
17✔
115
        if err != nil {
17✔
NEW
116
                return nil, err
×
NEW
117
        }
×
118
        return scheme, nil
17✔
119
}
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