Skip to content

Commit cd3cb2a

Browse files
authored
Merge pull request #1644 from estroz/feature/crd-webhook-v1
⚠️ (go/v3-alpha) default to v1 CRDs and webhooks
2 parents c4c8200 + 239c6d2 commit cd3cb2a

File tree

89 files changed

+1174
-853
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

89 files changed

+1174
-853
lines changed

docs/book/src/reference/makefile-helpers.md

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Makefile Helpers
22

3-
By default, the projects are scaffolded with a `Makefile`. You can customize and update this file as please you. Here, you will find some helpers that can be useful.
3+
By default, the projects are scaffolded with a `Makefile`. You can customize and update this file as please you. Here, you will find some helpers that can be useful.
44

55
## To debug with go-delve
66

@@ -14,28 +14,24 @@ run-delve: generate fmt vet manifests
1414
dlv --listen=:2345 --headless=true --api-version=2 --accept-multiclient exec ./bin/manager
1515
```
1616

17-
## To change the version of CRDs
17+
## To change the version of CRDs
1818

19-
The tool generate the CRDs by using [controller-tools](https://github.com/kubernetes-sigs/controller-tools), see in the manifests target:
19+
The `controller-gen` program (from [controller-tools](https://github.com/kubernetes-sigs/controller-tools))
20+
generates CRDs for kubebuilder projects, wrapped in the following `make` rule:
2021

2122
```sh
22-
# Generate manifests e.g. CRD, RBAC etc.
2323
manifests: controller-gen
2424
$(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
2525
```
2626

27-
In this way, update `CRD_OPTIONS` to define the version of the CRDs manifests which will be generated in the `config/crd/bases` directory:
27+
`controller-gen` lets you specify what CRD API version to generate (either "v1", the default, or "v1beta1").
28+
You can direct it to generate a specific version by adding `crd:crdVersions={<version>}` to your `CRD_OPTIONS`,
29+
found at the top of your Makefile:
2830

2931
```sh
30-
# Produce CRDs that work back to Kubernetes 1.11 (no version conversion)
31-
CRD_OPTIONS ?= "crd:trivialVersions=true"
32+
CRD_OPTIONS ?= "crd:crdVersions={v1beta1},trivialVersions=true,preserveUnknownFields=false"
3233
```
3334

34-
| CRD_OPTIONS | API version |
35-
|--- |--- |
36-
| `"crd:trivialVersions=true"` | `apiextensions.k8s.io/v1beta1` |
37-
| `"crd:crdVersions=v1"` | `apiextensions.k8s.io/v1` |
38-
3935
## To get all the manifests without deploying
4036

4137
By adding `make dry-run` you can get the patched manifests in the dry-run folder, unlike `make depĺoy` which runs `kustomize` and `kubectl apply`.

pkg/model/config/config.go

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -87,17 +87,19 @@ func (c Config) HasResource(target GVK) bool {
8787
return false
8888
}
8989

90-
// AddResource appends the provided resource to the tracked ones
91-
// It returns if the configuration was modified
92-
func (c *Config) AddResource(gvk GVK) bool {
93-
// No-op if the resource was already tracked, return false
94-
if c.HasResource(gvk) {
95-
return false
90+
// UpdateResources either adds gvk to the tracked set or, if the resource already exists,
91+
// updates the the equivalent resource in the set.
92+
func (c *Config) UpdateResources(gvk GVK) {
93+
// If the resource already exists, update it.
94+
for i, r := range c.Resources {
95+
if r.isEqualTo(gvk) {
96+
c.Resources[i].merge(gvk)
97+
return
98+
}
9699
}
97100

98-
// Append the resource to the tracked ones, return true
101+
// The resource does not exist, append the resource to the tracked ones.
99102
c.Resources = append(c.Resources, gvk)
100-
return true
101103
}
102104

103105
// HasGroup returns true if group is already tracked
@@ -113,11 +115,44 @@ func (c Config) HasGroup(group string) bool {
113115
return false
114116
}
115117

118+
// IsCRDVersionCompatible returns true if crdVersion can be added to the existing set of CRD versions.
119+
func (c Config) IsCRDVersionCompatible(crdVersion string) bool {
120+
return c.resourceAPIVersionCompatible("crd", crdVersion)
121+
}
122+
123+
// IsWebhookVersionCompatible returns true if webhookVersion can be added to the existing set of Webhook versions.
124+
func (c Config) IsWebhookVersionCompatible(webhookVersion string) bool {
125+
return c.resourceAPIVersionCompatible("webhook", webhookVersion)
126+
}
127+
128+
// resourceAPIVersionCompatible returns true if version can be added to the existing set of versions
129+
// for a given verType.
130+
func (c Config) resourceAPIVersionCompatible(verType, version string) bool {
131+
for _, res := range c.Resources {
132+
var currVersion string
133+
switch verType {
134+
case "crd":
135+
currVersion = res.CRDVersion
136+
case "webhook":
137+
currVersion = res.WebhookVersion
138+
}
139+
if currVersion != "" && version != currVersion {
140+
return false
141+
}
142+
}
143+
return true
144+
}
145+
116146
// GVK contains information about scaffolded resources
117147
type GVK struct {
118148
Group string `json:"group,omitempty"`
119149
Version string `json:"version,omitempty"`
120150
Kind string `json:"kind,omitempty"`
151+
152+
// CRDVersion holds the CustomResourceDefinition API version used for the GVK.
153+
CRDVersion string `json:"crdVersion,omitempty"`
154+
// WebhookVersion holds the {Validating,Mutating}WebhookConfiguration API version used for the GVK.
155+
WebhookVersion string `json:"webhookVersion,omitempty"`
121156
}
122157

123158
// isEqualTo compares it with another resource
@@ -127,6 +162,17 @@ func (r GVK) isEqualTo(other GVK) bool {
127162
r.Kind == other.Kind
128163
}
129164

165+
// merge combines fields of two GVKs that have matching group, version, and kind,
166+
// favoring the receiver's values.
167+
func (r *GVK) merge(other GVK) {
168+
if r.CRDVersion == "" && other.CRDVersion != "" {
169+
r.CRDVersion = other.CRDVersion
170+
}
171+
if r.WebhookVersion == "" && other.WebhookVersion != "" {
172+
r.WebhookVersion = other.WebhookVersion
173+
}
174+
}
175+
130176
// Marshal returns the bytes of c.
131177
func (c Config) Marshal() ([]byte, error) {
132178
// Ignore extra fields at first.

pkg/model/config/config_test.go

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ import (
2121
. "github.com/onsi/gomega"
2222
)
2323

24-
var _ = Describe("Config", func() {
24+
const v1beta1 = "v1beta1"
25+
26+
var _ = Describe("PluginConfig", func() {
2527
// Test plugin config. Don't want to export this config, but need it to
2628
// be accessible by test.
2729
type PluginConfig struct {
@@ -146,3 +148,94 @@ var _ = Describe("Config", func() {
146148
Expect(pluginConfig).To(Equal(expectedPluginConfig))
147149
})
148150
})
151+
152+
var _ = Describe("Resource Version Compatibility", func() {
153+
154+
var (
155+
c *Config
156+
gvk1, gvk2 GVK
157+
158+
defaultVersion = "v1"
159+
)
160+
161+
BeforeEach(func() {
162+
c = &Config{}
163+
gvk1 = GVK{Group: "example", Version: "v1", Kind: "TestKind"}
164+
gvk2 = GVK{Group: "example", Version: "v1", Kind: "TestKind2"}
165+
})
166+
167+
Context("resourceAPIVersionCompatible", func() {
168+
It("returns true for a list of empty resources", func() {
169+
Expect(c.resourceAPIVersionCompatible("crd", defaultVersion)).To(BeTrue())
170+
})
171+
It("returns true for one resource with an empty version", func() {
172+
c.Resources = []GVK{gvk1}
173+
Expect(c.resourceAPIVersionCompatible("crd", defaultVersion)).To(BeTrue())
174+
})
175+
It("returns true for one resource with matching version", func() {
176+
gvk1.CRDVersion = defaultVersion
177+
c.Resources = []GVK{gvk1}
178+
Expect(c.resourceAPIVersionCompatible("crd", defaultVersion)).To(BeTrue())
179+
})
180+
It("returns true for two resources with matching versions", func() {
181+
gvk1.CRDVersion = defaultVersion
182+
gvk2.CRDVersion = defaultVersion
183+
c.Resources = []GVK{gvk1, gvk2}
184+
Expect(c.resourceAPIVersionCompatible("crd", defaultVersion)).To(BeTrue())
185+
})
186+
It("returns false for one resource with a non-matching version", func() {
187+
gvk1.CRDVersion = v1beta1
188+
c.Resources = []GVK{gvk1}
189+
Expect(c.resourceAPIVersionCompatible("crd", defaultVersion)).To(BeFalse())
190+
})
191+
It("returns false for two resources containing a non-matching version", func() {
192+
gvk1.CRDVersion = v1beta1
193+
gvk2.CRDVersion = defaultVersion
194+
c.Resources = []GVK{gvk1, gvk2}
195+
Expect(c.resourceAPIVersionCompatible("crd", defaultVersion)).To(BeFalse())
196+
})
197+
198+
It("returns false for two resources containing a non-matching version (webhooks)", func() {
199+
gvk1.WebhookVersion = v1beta1
200+
gvk2.WebhookVersion = defaultVersion
201+
c.Resources = []GVK{gvk1, gvk2}
202+
Expect(c.resourceAPIVersionCompatible("webhook", defaultVersion)).To(BeFalse())
203+
})
204+
})
205+
})
206+
207+
var _ = Describe("Config", func() {
208+
var (
209+
c *Config
210+
gvk1, gvk2 GVK
211+
)
212+
213+
BeforeEach(func() {
214+
c = &Config{}
215+
gvk1 = GVK{Group: "example", Version: "v1", Kind: "TestKind"}
216+
gvk2 = GVK{Group: "example", Version: "v1", Kind: "TestKind2"}
217+
})
218+
219+
Context("UpdateResource", func() {
220+
It("Adds a non-existing resource", func() {
221+
c.UpdateResources(gvk1)
222+
Expect(c.Resources).To(Equal([]GVK{gvk1}))
223+
// Update again to ensure idempotency.
224+
c.UpdateResources(gvk1)
225+
Expect(c.Resources).To(Equal([]GVK{gvk1}))
226+
})
227+
It("Updates an existing resource", func() {
228+
c.UpdateResources(gvk1)
229+
gvk := GVK{Group: gvk1.Group, Version: gvk1.Version, Kind: gvk1.Kind, CRDVersion: "v1"}
230+
c.UpdateResources(gvk)
231+
Expect(c.Resources).To(Equal([]GVK{gvk}))
232+
})
233+
It("Updates an existing resource with more than one resource present", func() {
234+
c.UpdateResources(gvk1)
235+
c.UpdateResources(gvk2)
236+
gvk := GVK{Group: gvk1.Group, Version: gvk1.Version, Kind: gvk1.Kind, CRDVersion: "v1"}
237+
c.UpdateResources(gvk)
238+
Expect(c.Resources).To(Equal([]GVK{gvk, gvk2}))
239+
})
240+
})
241+
})

pkg/model/resource/options.go

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ type Options struct {
8282

8383
// Namespaced is true if the resource is namespaced.
8484
Namespaced bool
85+
86+
// CRDVersion holds the CustomResourceDefinition API version used for the Options.
87+
CRDVersion string
88+
// WebhookVersion holds the {Validating,Mutating}WebhookConfiguration API version used for the Options.
89+
WebhookVersion string
8590
}
8691

8792
// ValidateV2 verifies that V2 project has all the fields have valid values
@@ -183,6 +188,18 @@ func (opts *Options) Validate() error {
183188
return fmt.Errorf("invalid Kind: %#v", validationErrors)
184189
}
185190

191+
// Ensure apiVersions for k8s types are empty or valid.
192+
for typ, apiVersion := range map[string]string{
193+
"CRD": opts.CRDVersion,
194+
"Webhook": opts.WebhookVersion,
195+
} {
196+
switch apiVersion {
197+
case "", "v1", "v1beta1":
198+
default:
199+
return fmt.Errorf("%s version must be one of: v1, v1beta1", typ)
200+
}
201+
}
202+
186203
// TODO: validate plural strings if provided
187204

188205
return nil
@@ -191,9 +208,11 @@ func (opts *Options) Validate() error {
191208
// GVK returns the group-version-kind information to check against tracked resources in the configuration file
192209
func (opts *Options) GVK() config.GVK {
193210
return config.GVK{
194-
Group: opts.Group,
195-
Version: opts.Version,
196-
Kind: opts.Kind,
211+
Group: opts.Group,
212+
Version: opts.Version,
213+
Kind: opts.Kind,
214+
CRDVersion: opts.CRDVersion,
215+
WebhookVersion: opts.WebhookVersion,
197216
}
198217
}
199218

@@ -269,5 +288,7 @@ func (opts *Options) newResource() *Resource {
269288
Kind: opts.Kind,
270289
Plural: plural,
271290
ImportAlias: opts.safeImport(opts.Group + opts.Version),
291+
CRDVersion: opts.CRDVersion,
292+
WebhookVersion: opts.WebhookVersion,
272293
}
273294
}

pkg/model/resource/resource.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,21 @@ type Resource struct {
5151

5252
// Namespaced is true if the resource is namespaced.
5353
Namespaced bool `json:"namespaced,omitempty"`
54+
55+
// CRDVersion holds the CustomResourceDefinition API version used for the Resource.
56+
CRDVersion string `json:"crdVersion,omitempty"`
57+
// WebhookVersion holds the {Validating,Mutating}WebhookConfiguration API version used for the Resource.
58+
WebhookVersion string `json:"webhookVersion,omitempty"`
5459
}
5560

5661
// GVK returns the group-version-kind information to check against tracked resources in the configuration file
5762
func (r *Resource) GVK() config.GVK {
5863
return config.GVK{
59-
Group: r.Group,
60-
Version: r.Version,
61-
Kind: r.Kind,
64+
Group: r.Group,
65+
Version: r.Version,
66+
Kind: r.Kind,
67+
CRDVersion: r.CRDVersion,
68+
WebhookVersion: r.WebhookVersion,
6269
}
6370
}
6471

pkg/plugin/v2/scaffolds/api.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ func (s *apiScaffolder) newUniverse() *model.Universe {
9393

9494
func (s *apiScaffolder) scaffold() error {
9595
if s.doResource {
96-
s.config.AddResource(s.resource.GVK())
96+
s.config.UpdateResources(s.resource.GVK())
9797

9898
if err := machinery.NewScaffold(s.plugins...).Execute(
9999
s.newUniverse(),

pkg/plugin/v3/api.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,15 @@ import (
3737
"sigs.k8s.io/kubebuilder/v2/plugins/addon"
3838
)
3939

40-
// KbDeclarativePatternVersion is the sigs.k8s.io/kubebuilder-declarative-pattern version
41-
// (used only to gen api with --pattern=addon)
42-
// TODO: remove this when a better solution for using addons is implemented.
43-
const KbDeclarativePatternVersion = "1cbf859290cab81ae8e73fc5caebe792280175d1"
40+
const (
41+
// KbDeclarativePatternVersion is the sigs.k8s.io/kubebuilder-declarative-pattern version
42+
// (used only to gen api with --pattern=addon)
43+
// TODO: remove this when a better solution for using addons is implemented.
44+
KbDeclarativePatternVersion = "1cbf859290cab81ae8e73fc5caebe792280175d1"
45+
46+
// defaultCRDVersion is the default CRD API version to scaffold.
47+
defaultCRDVersion = "v1"
48+
)
4449

4550
// DefaultMainPath is default file path of main.go
4651
const DefaultMainPath = "main.go"
@@ -124,6 +129,8 @@ func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) {
124129
fs.StringVar(&p.resource.Group, "group", "", "resource Group")
125130
fs.StringVar(&p.resource.Version, "version", "", "resource Version")
126131
fs.BoolVar(&p.resource.Namespaced, "namespaced", true, "resource is namespaced")
132+
fs.StringVar(&p.resource.CRDVersion, "crd-version", defaultCRDVersion,
133+
"version of CustomResourceDefinition to scaffold. Options: [v1, v1beta1]")
127134
}
128135

129136
func (p *createAPISubcommand) InjectConfig(c *config.Config) {
@@ -172,6 +179,12 @@ func (p *createAPISubcommand) Validate() error {
172179
return fmt.Errorf("multiple groups are not allowed by default, " +
173180
"to enable multi-group visit kubebuilder.io/migration/multi-group.html")
174181
}
182+
183+
// Check CRDVersion against all other CRDVersions in p.config for compatibility.
184+
if !p.config.IsCRDVersionCompatible(p.resource.CRDVersion) {
185+
return fmt.Errorf("only one CRD version can be used for all resources, cannot add %q",
186+
p.resource.CRDVersion)
187+
}
175188
}
176189

177190
return nil

0 commit comments

Comments
 (0)