Skip to content

Commit cdcfebb

Browse files
committed
crd-gen support multiple CRD encodings/CRD v1
This updates crd-gen to support generating CRDs of multiple "encodings" of the CRD itself (i.e. versions of the CRD object). Currently that means v1 and v1beta1. It defaults to all known versions, each getting their own file. The first version specified (v1beta1 by default) gets a non-suffixed, file, the rest are suffixed with their version.
1 parent 82b955a commit cdcfebb

File tree

5 files changed

+187
-100
lines changed

5 files changed

+187
-100
lines changed

pkg/crd/conv.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package crd
2+
3+
import (
4+
"fmt"
5+
6+
apiextinternal "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
7+
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
8+
apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
9+
"k8s.io/apimachinery/pkg/api/equality"
10+
"k8s.io/apimachinery/pkg/runtime"
11+
"k8s.io/apimachinery/pkg/runtime/schema"
12+
)
13+
14+
var (
15+
conversionScheme = runtime.NewScheme()
16+
)
17+
18+
func init() {
19+
if err := apiextinternal.AddToScheme(conversionScheme); err != nil {
20+
panic("must be able to add internal apiextensions to the CRD conversion Scheme")
21+
}
22+
if err := apiext.AddToScheme(conversionScheme); err != nil {
23+
panic("must be able to add apiextensions/v1 to the CRD conversion Scheme")
24+
}
25+
if err := apiextv1beta1.AddToScheme(conversionScheme); err != nil {
26+
panic("must be able to add apiextensions/v1beta1 to the CRD conversion Scheme")
27+
}
28+
}
29+
30+
// AsVersion converts a CRD from the canonical internal form (currently v1) to some external form.
31+
func AsVersion(original apiext.CustomResourceDefinition, gv schema.GroupVersion) (runtime.Object, error) {
32+
// We can use the internal versions an existing conversions from kubernetes, since they're not in k/k itself.
33+
// This punts the problem of conversion down the road for a future maintainer (or future instance of @directxman12)
34+
// when we have to support older versions that get removed, or when API machinery decides to yell at us for this
35+
// questionable decision.
36+
intVer, err := conversionScheme.ConvertToVersion(&original, apiextinternal.SchemeGroupVersion)
37+
if err != nil {
38+
return nil, fmt.Errorf("unable to convert to internal CRD version: %v", err)
39+
}
40+
41+
return conversionScheme.ConvertToVersion(intVer, gv)
42+
}
43+
44+
// mergeIdenticalSubresources checks to see if subresources are identical across
45+
// all versions, and if so, merges them into a top-level version.
46+
//
47+
// This assumes you're not using trivial versions.
48+
func mergeIdenticalSubresources(crd *apiextv1beta1.CustomResourceDefinition) {
49+
subres := crd.Spec.Versions[0].Subresources
50+
for _, ver := range crd.Spec.Versions {
51+
if ver.Subresources == nil || !equality.Semantic.DeepEqual(subres, ver.Subresources) {
52+
// either all nil, or not identical
53+
return
54+
}
55+
}
56+
57+
// things are identical if we've gotten this far, so move the subresources up
58+
// and discard the identical per-version ones
59+
crd.Spec.Subresources = subres
60+
for i := range crd.Spec.Versions {
61+
crd.Spec.Versions[i].Subresources = nil
62+
}
63+
}
64+
65+
// mergeIdenticalSchemata checks to see if schemata are identical across
66+
// all versions, and if so, merges them into a top-level version.
67+
//
68+
// This assumes you're not using trivial versions.
69+
func mergeIdenticalSchemata(crd *apiextv1beta1.CustomResourceDefinition) {
70+
schema := crd.Spec.Versions[0].Schema
71+
for _, ver := range crd.Spec.Versions {
72+
if ver.Schema == nil || !equality.Semantic.DeepEqual(schema, ver.Schema) {
73+
// either all nil, or not identical
74+
return
75+
}
76+
}
77+
78+
// things are identical if we've gotten this far, so move the schemata up
79+
// to a single schema and discard the identical per-version ones
80+
crd.Spec.Validation = schema
81+
for i := range crd.Spec.Versions {
82+
crd.Spec.Versions[i].Schema = nil
83+
}
84+
}
85+
86+
// mergeIdenticalPrinterColumns checks to see if schemata are identical across
87+
// all versions, and if so, merges them into a top-level version.
88+
//
89+
// This assumes you're not using trivial versions.
90+
func mergeIdenticalPrinterColumns(crd *apiextv1beta1.CustomResourceDefinition) {
91+
cols := crd.Spec.Versions[0].AdditionalPrinterColumns
92+
for _, ver := range crd.Spec.Versions {
93+
if len(ver.AdditionalPrinterColumns) == 0 || !equality.Semantic.DeepEqual(cols, ver.AdditionalPrinterColumns) {
94+
// either all nil, or not identical
95+
return
96+
}
97+
}
98+
99+
// things are identical if we've gotten this far, so move the printer columns up
100+
// and discard the identical per-version ones
101+
crd.Spec.AdditionalPrinterColumns = cols
102+
for i := range crd.Spec.Versions {
103+
crd.Spec.Versions[i].AdditionalPrinterColumns = nil
104+
}
105+
}
106+
107+
// MergeIdenticalVersionInfo makes sure that components of the Versions field that are identical
108+
// across all versions get merged into the top-level fields in v1beta1.
109+
//
110+
// This is required by the Kubernetes API server validation.
111+
//
112+
// The reason is that a v1beta1 -> v1 -> v1beta1 conversion cycle would need to
113+
// round-trip identically, v1 doesn't have top-level subresources, and without
114+
// this restriction it would be ambiguous how a v1-with-identical-subresources
115+
// converts into a v1beta1).
116+
func MergeIdenticalVersionInfo(crd *apiextv1beta1.CustomResourceDefinition) {
117+
if len(crd.Spec.Versions) > 0 {
118+
mergeIdenticalSubresources(crd)
119+
mergeIdenticalSchemata(crd)
120+
mergeIdenticalPrinterColumns(crd)
121+
}
122+
}

pkg/crd/gen.go

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ import (
2020
"fmt"
2121
"go/types"
2222

23-
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
23+
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
24+
apiextlegacy "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
2425
"k8s.io/apimachinery/pkg/runtime/schema"
2526

2627
crdmarkers "sigs.k8s.io/controller-tools/pkg/crd/markers"
@@ -39,6 +40,8 @@ type Generator struct {
3940
// Single "trivial-version" CRDs are compatible with older (pre 1.13)
4041
// Kubernetes API servers. The storage version's schema will be used as
4142
// the CRD's schema.
43+
//
44+
// Only works with the v1beta1 CRD version.
4245
TrivialVersions bool `marker:",optional"`
4346

4447
// MaxDescLen specifies the maximum description length for fields in CRD's OpenAPI schema.
@@ -47,6 +50,16 @@ type Generator struct {
4750
// n indicates limit the description to at most n characters and truncate the description to
4851
// closest sentence boundary if it exceeds n characters.
4952
MaxDescLen *int `marker:",optional"`
53+
54+
// CRDVersions specifies the target API versions of the CRD type itself to
55+
// generate. Defaults to v1beta1.
56+
//
57+
// The first version listed will be assumed to be the "default" version and
58+
// will not get a version suffix in the output filename.
59+
//
60+
// You'll need to use "v1" to get support for features like defaulting,
61+
// along with an API server that supports it (Kubernetes 1.16+).
62+
CRDVersions []string `marker:"crdVersions,optional"`
5063
}
5164

5265
func (Generator) RegisterMarkers(into *markers.Registry) error {
@@ -76,16 +89,44 @@ func (g Generator) Generate(ctx *genall.GenerationContext) error {
7689
return nil
7790
}
7891

92+
crdVersions := g.CRDVersions
93+
94+
if len(crdVersions) == 0 {
95+
crdVersions = []string{"v1beta1"}
96+
}
97+
7998
for _, groupKind := range kubeKinds {
8099
parser.NeedCRDFor(groupKind, g.MaxDescLen)
81-
crd := parser.CustomResourceDefinitions[groupKind]
100+
crdRaw := parser.CustomResourceDefinitions[groupKind]
101+
addAttribution(&crdRaw)
102+
103+
versionedCRDs := make([]interface{}, len(crdVersions))
104+
for i, ver := range crdVersions {
105+
conv, err := AsVersion(crdRaw, schema.GroupVersion{Group: apiext.SchemeGroupVersion.Group, Version: ver})
106+
if err != nil {
107+
return err
108+
}
109+
versionedCRDs[i] = conv
110+
}
111+
82112
if g.TrivialVersions {
83-
toTrivialVersions(&crd)
113+
for i, crd := range versionedCRDs {
114+
if crdVersions[i] == "v1beta1" {
115+
toTrivialVersions(crd.(*apiextlegacy.CustomResourceDefinition))
116+
}
117+
}
84118
}
85-
addAttribution(&crd)
86-
fileName := fmt.Sprintf("%s_%s.yaml", crd.Spec.Group, crd.Spec.Names.Plural)
87-
if err := ctx.WriteYAML(fileName, crd); err != nil {
88-
return err
119+
120+
for i, crd := range versionedCRDs {
121+
var fileName string
122+
if i == 0 {
123+
fileName = fmt.Sprintf("%s_%s.yaml", crdRaw.Spec.Group, crdRaw.Spec.Names.Plural)
124+
} else {
125+
fileName = fmt.Sprintf("%s_%s.%s.yaml", crdRaw.Spec.Group, crdRaw.Spec.Names.Plural, crdVersions[i])
126+
}
127+
if err := ctx.WriteYAML(fileName, crd); err != nil {
128+
return err
129+
}
89130
}
90131
}
91132

@@ -95,10 +136,10 @@ func (g Generator) Generate(ctx *genall.GenerationContext) error {
95136
// toTrivialVersions strips out all schemata except for the storage schema,
96137
// and moves that up into the root object. This makes the CRD compatible
97138
// with pre 1.13 clusters.
98-
func toTrivialVersions(crd *apiext.CustomResourceDefinition) {
99-
var canonicalSchema *apiext.CustomResourceValidation
100-
var canonicalSubresources *apiext.CustomResourceSubresources
101-
var canonicalColumns []apiext.CustomResourceColumnDefinition
139+
func toTrivialVersions(crd *apiextlegacy.CustomResourceDefinition) {
140+
var canonicalSchema *apiextlegacy.CustomResourceValidation
141+
var canonicalSubresources *apiextlegacy.CustomResourceSubresources
142+
var canonicalColumns []apiextlegacy.CustomResourceColumnDefinition
102143
for i, ver := range crd.Spec.Versions {
103144
if ver.Storage == true {
104145
canonicalSchema = ver.Schema

pkg/crd/parser_integration_test.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import (
2424
. "github.com/onsi/ginkgo"
2525
. "github.com/onsi/gomega"
2626
"golang.org/x/tools/go/packages"
27-
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
27+
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
2828
"k8s.io/apimachinery/pkg/runtime/schema"
2929
"sigs.k8s.io/yaml"
3030

@@ -53,6 +53,9 @@ func packageErrors(pkg *loader.Package, filterKinds ...packages.ErrorKind) error
5353

5454
var _ = Describe("CRD Generation From Parsing to CustomResourceDefinition", func() {
5555
It("should properly generate and flatten the rewritten CronJob schema", func() {
56+
// TODO(directxman12): test generation across multiple versions (right
57+
// now, we're trusting k/k's conversion code, though, which is probably
58+
// fine for the time being)
5659
By("switching into testdata to appease go modules")
5760
cwd, err := os.Getwd()
5861
Expect(err).NotTo(HaveOccurred())
@@ -94,6 +97,8 @@ var _ = Describe("CRD Generation From Parsing to CustomResourceDefinition", func
9497
By("parsing the desired YAML")
9598
var crd apiext.CustomResourceDefinition
9699
Expect(yaml.Unmarshal(expectedFile, &crd)).To(Succeed())
100+
// clear the annotations -- we don't care about the attribution annotation
101+
crd.Annotations = nil
97102

98103
By("comparing the two")
99104
Expect(parser.CustomResourceDefinitions[groupKind]).To(Equal(crd), "type not as expected, check pkg/crd/testdata/README.md for more details.\n\nDiff:\n\n%s", cmp.Diff(parser.CustomResourceDefinitions[groupKind], crd))

pkg/crd/spec.go

Lines changed: 2 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ import (
2222

2323
"github.com/gobuffalo/flect"
2424

25-
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
26-
"k8s.io/apimachinery/pkg/api/equality"
25+
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
2726
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2827
"k8s.io/apimachinery/pkg/runtime/schema"
2928

@@ -38,86 +37,6 @@ type SpecMarker interface {
3837
ApplyToCRD(crd *apiext.CustomResourceDefinitionSpec, version string) error
3938
}
4039

41-
// mergeIdenticalSubresources checks to see if subresources are identical across
42-
// all versions, and if so, merges them into a top-level version.
43-
//
44-
// This assumes you're not using trivial versions.
45-
func mergeIdenticalSubresources(crd *apiext.CustomResourceDefinition) {
46-
subres := crd.Spec.Versions[0].Subresources
47-
for _, ver := range crd.Spec.Versions {
48-
if ver.Subresources == nil || !equality.Semantic.DeepEqual(subres, ver.Subresources) {
49-
// either all nil, or not identical
50-
return
51-
}
52-
}
53-
54-
// things are identical if we've gotten this far, so move the subresources up
55-
// and discard the identical per-version ones
56-
crd.Spec.Subresources = subres
57-
for i := range crd.Spec.Versions {
58-
crd.Spec.Versions[i].Subresources = nil
59-
}
60-
}
61-
62-
// mergeIdenticalSchemata checks to see if schemata are identical across
63-
// all versions, and if so, merges them into a top-level version.
64-
//
65-
// This assumes you're not using trivial versions.
66-
func mergeIdenticalSchemata(crd *apiext.CustomResourceDefinition) {
67-
schema := crd.Spec.Versions[0].Schema
68-
for _, ver := range crd.Spec.Versions {
69-
if ver.Schema == nil || !equality.Semantic.DeepEqual(schema, ver.Schema) {
70-
// either all nil, or not identical
71-
return
72-
}
73-
}
74-
75-
// things are identical if we've gotten this far, so move the schemata up
76-
// to a single schema and discard the identical per-version ones
77-
crd.Spec.Validation = schema
78-
for i := range crd.Spec.Versions {
79-
crd.Spec.Versions[i].Schema = nil
80-
}
81-
}
82-
83-
// mergeIdenticalPrinterColumns checks to see if schemata are identical across
84-
// all versions, and if so, merges them into a top-level version.
85-
//
86-
// This assumes you're not using trivial versions.
87-
func mergeIdenticalPrinterColumns(crd *apiext.CustomResourceDefinition) {
88-
cols := crd.Spec.Versions[0].AdditionalPrinterColumns
89-
for _, ver := range crd.Spec.Versions {
90-
if len(ver.AdditionalPrinterColumns) == 0 || !equality.Semantic.DeepEqual(cols, ver.AdditionalPrinterColumns) {
91-
// either all nil, or not identical
92-
return
93-
}
94-
}
95-
96-
// things are identical if we've gotten this far, so move the printer columns up
97-
// and discard the identical per-version ones
98-
crd.Spec.AdditionalPrinterColumns = cols
99-
for i := range crd.Spec.Versions {
100-
crd.Spec.Versions[i].AdditionalPrinterColumns = nil
101-
}
102-
}
103-
104-
// MergeIdenticalVersionInfo makes sure that components of the Versions field that are identical
105-
// across all versions get merged into the top-level fields in v1beta1.
106-
//
107-
// This is required by the Kubernetes API server validation.
108-
//
109-
// The reason is that a v1beta1 -> v1 -> v1beta1 conversion cycle would need to
110-
// round-trip identically, v1 doesn't have top-level subresources, and without
111-
// this restriction it would be ambiguous how a v1-with-identical-subresources
112-
// converts into a v1beta1).
113-
func MergeIdenticalVersionInfo(crd *apiext.CustomResourceDefinition) {
114-
if len(crd.Spec.Versions) > 0 {
115-
mergeIdenticalSubresources(crd)
116-
mergeIdenticalSchemata(crd)
117-
mergeIdenticalPrinterColumns(crd)
118-
}
119-
}
120-
12140
// NeedCRDFor requests the full CRD for the given group-kind. It requires
12241
// that the packages containing the Go structs for that CRD have already
12342
// been loaded with NeedPackage.
@@ -153,6 +72,7 @@ func (p *Parser) NeedCRDFor(groupKind schema.GroupKind, maxDescLen *int) {
15372
Plural: defaultPlural,
15473
Singular: strings.ToLower(groupKind.Kind),
15574
},
75+
Scope: apiext.NamespaceScoped,
15676
},
15777
}
15878

@@ -211,7 +131,6 @@ func (p *Parser) NeedCRDFor(groupKind schema.GroupKind, maxDescLen *int) {
211131
// it is necessary to make sure the order of CRD versions in crd.Spec.Versions is stable and explicitly set crd.Spec.Version.
212132
// Otherwise, crd.Spec.Version may point to different CRD versions across different runs.
213133
sort.Slice(crd.Spec.Versions, func(i, j int) bool { return crd.Spec.Versions[i].Name < crd.Spec.Versions[j].Name })
214-
crd.Spec.Version = crd.Spec.Versions[0].Name
215134

216135
// make sure we have *a* storage version
217136
// (default it if we only have one, otherwise, bail)
@@ -238,9 +157,5 @@ func (p *Parser) NeedCRDFor(groupKind schema.GroupKind, maxDescLen *int) {
238157
crd.Status.Conditions = []apiext.CustomResourceDefinitionCondition{}
239158
crd.Status.StoredVersions = []string{}
240159

241-
// make sure we merge identical per-version parts, to avoid validation errors
242-
// (see the reasoning near the top of the file).
243-
MergeIdenticalVersionInfo(&crd)
244-
245160
p.CustomResourceDefinitions[groupKind] = crd
246161
}

pkg/crd/zz_generated.markerhelp.go

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)