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

kubernetes-sigs / kubebuilder / 23042341438

13 Mar 2026 08:20AM UTC coverage: 79.145% (-0.6%) from 79.762%
23042341438

Pull #5539

github

camilamacedo86
chore(go/v4): Add support for multiple controllers per GVK

Change API to allow it via a `controllers[]` list in the PROJECT file, with a new `--controller-name` flag for scaffolding additional controllers while maintaining full backward compatibility with existing `controller: true` behavior.

Generated-by: Claude
Pull Request #5539: WIP ✨ (go/v4): Add support for multiple controllers per GVK

116 of 203 new or added lines in 4 files covered. (57.14%)

28 existing lines in 3 files now uncovered.

6019 of 7605 relevant lines covered (79.15%)

35.2 hits per line

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

67.23
/pkg/model/resource/controller.go
1
/*
2
Copyright 2026 The Kubernetes Authors.
3

4
Licensed under the Apache License, Version 2.0 (the "License");
5
you may not use this file except in compliance with the License.
6
You may obtain a copy of the License at
7

8
    http://www.apache.org/licenses/LICENSE-2.0
9

10
Unless required by applicable law or agreed to in writing, software
11
distributed under the License is distributed on an "AS IS" BASIS,
12
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
See the License for the specific language governing permissions and
14
limitations under the License.
15
*/
16

17
package resource
18

19
import (
20
        "fmt"
21
        "strings"
22

23
        "k8s.io/apimachinery/pkg/util/validation"
24
)
25

26
// Controller represents a named controller for a resource.
27
type Controller struct {
28
        // Name is the unique controller identifier within a GVK.
29
        Name string `json:"name,omitempty"`
30
}
31

32
// Validate checks that the Controller is valid.
33
func (c Controller) Validate() error {
17✔
34
        // Validate the Name
17✔
35
        if c.Name == "" {
18✔
36
                return fmt.Errorf("controller name cannot be empty")
1✔
37
        }
1✔
38

39
        // Controller names should be valid DNS labels
40
        if errors := validation.IsDNS1035Label(c.Name); len(errors) != 0 {
20✔
41
                return fmt.Errorf("invalid controller name %q: %s", c.Name, strings.Join(errors, ", "))
4✔
42
        }
4✔
43

44
        return nil
12✔
45
}
46

47
// Controllers holds a list of controllers for a resource.
48
type Controllers []Controller
49

50
// IsEmpty returns true if there are no controllers.
51
func (c *Controllers) IsEmpty() bool {
22✔
52
        return c == nil || len(*c) == 0
22✔
53
}
22✔
54

55
// Validate checks that all controllers are valid with unique names.
56
// Detects normalization collisions (e.g., "captain-backup" vs "captainbackup").
57
func (c *Controllers) Validate() error {
7✔
58
        if c.IsEmpty() {
9✔
59
                return nil
2✔
60
        }
2✔
61

62
        names := make(map[string]bool)
5✔
63
        normalizedNames := make(map[string]string) // normalized -> original name
5✔
64

5✔
65
        for _, controller := range *c {
14✔
66
                if err := controller.Validate(); err != nil {
10✔
67
                        return err
1✔
68
                }
1✔
69

70
                // Check for duplicate names
71
                if names[controller.Name] {
10✔
72
                        return fmt.Errorf("duplicate controller name %q", controller.Name)
2✔
73
                }
2✔
74
                names[controller.Name] = true
6✔
75

6✔
76
                // Check for normalization collisions (e.g., "captain-backup" vs "captainbackup")
6✔
77
                normalized := normalizeControllerName(controller.Name)
6✔
78
                if existingName, exists := normalizedNames[normalized]; exists {
7✔
79
                        return fmt.Errorf("controller name %q conflicts with %q: both normalize to %q",
1✔
80
                                controller.Name, existingName, normalized+"Reconciler")
1✔
81
                }
1✔
82
                normalizedNames[normalized] = controller.Name
5✔
83
        }
84

85
        return nil
1✔
86
}
87

88
// normalizeControllerName removes non-alphanumeric chars and lowercases for collision detection.
89
func normalizeControllerName(name string) string {
6✔
90
        var result strings.Builder
6✔
91
        for _, r := range name {
82✔
92
                if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
148✔
93
                        result.WriteRune(r)
72✔
94
                }
72✔
95
        }
96
        return strings.ToLower(result.String())
6✔
97
}
98

99
// NormalizeFileName converts a controller name to a valid Go filename.
100
// Replaces hyphens with underscores for Go file naming conventions.
NEW
101
func NormalizeFileName(controllerName string) string {
×
NEW
102
        return strings.ReplaceAll(controllerName, "-", "_")
×
NEW
103
}
×
104

105
// NormalizeReconcilerName converts a controller name to PascalCase for the reconciler struct name.
106
// For backwards compatibility, returns "{Kind}Reconciler" if name matches lowercase kind.
NEW
107
func NormalizeReconcilerName(controllerName, kind string) string {
×
NEW
108
        // Backwards compatible: no controller name or controller name matches kind
×
NEW
109
        if controllerName == "" || controllerName == strings.ToLower(kind) {
×
NEW
110
                return kind + "Reconciler"
×
NEW
111
        }
×
112

113
        // Convert controller name (e.g., "captain-backup") to PascalCase (e.g., "CaptainBackup")
NEW
114
        parts := strings.Split(controllerName, "-")
×
NEW
115
        var result strings.Builder
×
NEW
116
        for _, part := range parts {
×
NEW
117
                if len(part) > 0 {
×
NEW
118
                        result.WriteString(strings.ToUpper(part[:1]) + part[1:])
×
NEW
119
                }
×
120
        }
NEW
121
        return result.String() + "Reconciler"
×
122
}
123

124
// GetControllerName returns the controller runtime name used in Named() and error logs.
125
// For multigroup projects, the group name is prefixed.
NEW
126
func GetControllerName(controllerName, kind, group string, multiGroup bool) string {
×
NEW
127
        var name string
×
NEW
128
        if controllerName != "" {
×
NEW
129
                name = controllerName
×
NEW
130
        } else {
×
NEW
131
                name = strings.ToLower(kind)
×
NEW
132
        }
×
133

134
        // Multigroup: prefix with group name
NEW
135
        if multiGroup && group != "" {
×
NEW
136
                return strings.ToLower(group) + "-" + name
×
NEW
137
        }
×
138

NEW
139
        return name
×
140
}
141

142
// HasController returns true if a controller with the given name exists.
143
func (c *Controllers) HasController(name string) bool {
7✔
144
        if c.IsEmpty() {
9✔
145
                return false
2✔
146
        }
2✔
147

148
        for _, controller := range *c {
12✔
149
                if controller.Name == name {
10✔
150
                        return true
3✔
151
                }
3✔
152
        }
153
        return false
2✔
154
}
155

156
// AddController adds a new controller with the given name.
157
// Returns an error if a controller with that name already exists.
158
func (c *Controllers) AddController(name string) error {
4✔
159
        if c == nil {
5✔
160
                return fmt.Errorf("cannot add controller to nil Controllers")
1✔
161
        }
1✔
162

163
        controller := Controller{Name: name}
3✔
164
        if err := controller.Validate(); err != nil {
4✔
165
                return err
1✔
166
        }
1✔
167

168
        if c.HasController(name) {
3✔
169
                return fmt.Errorf("controller with name %q already exists", name)
1✔
170
        }
1✔
171

172
        *c = append(*c, controller)
1✔
173
        return nil
1✔
174
}
175

176
// GetControllerNames returns a slice of all controller names.
177
func (c *Controllers) GetControllerNames() []string {
2✔
178
        if c.IsEmpty() {
2✔
NEW
179
                return nil
×
NEW
180
        }
×
181

182
        names := make([]string, 0, len(*c))
2✔
183
        for _, controller := range *c {
5✔
184
                names = append(names, controller.Name)
3✔
185
        }
3✔
186
        return names
2✔
187
}
188

189
// Copy returns a deep copy of the Controllers.
NEW
190
func (c *Controllers) Copy() Controllers {
×
NEW
191
        if c == nil {
×
NEW
192
                return Controllers{}
×
NEW
193
        }
×
194

NEW
195
        controllers := make(Controllers, len(*c))
×
NEW
196
        copy(controllers, *c)
×
NEW
197
        return controllers
×
198
}
199

200
// Update combines fields of two Controllers.
201
// It adds controllers from other that don't exist in c.
202
func (c *Controllers) Update(other *Controllers) error {
2✔
203
        if c == nil {
2✔
NEW
204
                return fmt.Errorf("cannot update a nil Controllers")
×
NEW
205
        }
×
206

207
        if other == nil || other.IsEmpty() {
2✔
NEW
208
                return nil
×
NEW
209
        }
×
210

211
        for _, controller := range *other {
4✔
212
                if !c.HasController(controller.Name) {
4✔
213
                        *c = append(*c, controller)
2✔
214
                }
2✔
215
        }
216

217
        return nil
2✔
218
}
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