Skip to content

Commit 13817b7

Browse files
committed
fix: support creating v1beta CRDs to avoid data loss during conversion to v1
1 parent 9f126de commit 13817b7

File tree

3 files changed

+104
-13
lines changed

3 files changed

+104
-13
lines changed

pkg/controller/operators/catalog/step.go

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,39 @@ func (b *builder) NewCRDStep(manifest string, client clientset.Interface, status
9898
return v1alpha1.StepStatusCreated, nil
9999
}
100100
case v1alpha1.StepStatusUnknown, v1alpha1.StepStatusNotPresent:
101-
crd, err := crdlib.Serialize(manifest)
101+
// data loss converting from v1beta1 -> v1 CRDs requires we create v1beta1 CRDs with the v1 client
102+
// first establish APIVersion of provided CRD
103+
var crdi interface{}
104+
var createError error
105+
version, err := crdlib.Version(&manifest)
102106
if err != nil {
103107
return v1alpha1.StepStatusUnknown, err
104108
}
109+
if version == crdlib.V1Version {
110+
crd, err := crdlib.SerializeV1(manifest)
111+
if err != nil {
112+
return v1alpha1.StepStatusUnknown, err
113+
}
114+
_, createError = client.ApiextensionsV1().CustomResourceDefinitions().Create(context.TODO(), crd, metav1.CreateOptions{})
115+
crdi = crd
116+
} else if version == crdlib.V1Beta1Version {
117+
crd, err := crdlib.SerializeV1Beta1(manifest)
118+
if err != nil {
119+
return v1alpha1.StepStatusUnknown, err
120+
}
121+
_, createError = client.ApiextensionsV1beta1().CustomResourceDefinitions().Create(context.TODO(), crd, metav1.CreateOptions{})
122+
crdi = crd
123+
} else {
124+
return v1alpha1.StepStatusUnknown, fmt.Errorf("unkown CRD version: %s", version)
125+
}
126+
127+
// convert to v1 type for remaining CRD reconciliation logic
128+
crd, err := crdlib.SerializeV1FromExisting(crdi)
129+
if err != nil {
130+
return v1alpha1.StepStatusUnknown, fmt.Errorf("unable to serialize into v1 CRD from existing manifest: %s", err)
131+
}
105132

106-
_, err = client.ApiextensionsV1().CustomResourceDefinitions().Create(context.TODO(), crd, metav1.CreateOptions{})
107-
if k8serrors.IsAlreadyExists(err) {
133+
if k8serrors.IsAlreadyExists(createError) {
108134
currentCRD, _ := client.ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), crd.GetName(), metav1.GetOptions{})
109135
// Compare 2 CRDs to see if it needs to be updatetd
110136
if crdlib.NotEqual(currentCRD, crd) {
@@ -149,7 +175,7 @@ func (b *builder) NewCRDStep(manifest string, client clientset.Interface, status
149175
// If it already existed, mark the step as Present.
150176
// they were equal - mark CRD as present
151177
return v1alpha1.StepStatusPresent, nil
152-
} else if err != nil {
178+
} else if createError != nil {
153179
// Unexpected error creating the CRD.
154180
return v1alpha1.StepStatusUnknown, err
155181
}

pkg/lib/crd/serialize.go

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@ import (
77

88
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/install"
99
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
10+
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
1011
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
1112
"k8s.io/apimachinery/pkg/runtime"
1213
"k8s.io/apimachinery/pkg/util/yaml"
1314
)
1415

1516
const (
16-
Kind = "CustomResourceDefinition"
17-
APIVersion = "apiextensions.k8s.io/v1"
17+
Kind = "CustomResourceDefinition"
18+
Group = "apiextensions.k8s.io/"
1819
)
1920

2021
var (
@@ -26,17 +27,16 @@ func init() {
2627
install.Install(scheme)
2728
}
2829

29-
// Serialize takes in a CRD manifest and returns a v1 versioned CRD object.
30+
// SerializeV1 takes in a CRD manifest and returns a v1 versioned CRD object.
3031
// Compatible with v1beta1 or v1 CRD manifests.
31-
func Serialize(manifest string) (*apiextensionsv1.CustomResourceDefinition, error) {
32+
func SerializeV1(manifest string) (*apiextensionsv1.CustomResourceDefinition, error) {
3233
u := &unstructured.Unstructured{}
3334
reader := bytes.NewReader([]byte(manifest))
3435
decoder := yaml.NewYAMLOrJSONDecoder(reader, 30)
3536
if err := decoder.Decode(u); err != nil {
3637
return nil, fmt.Errorf("crd unmarshaling failed: %s", err)
3738
}
3839

39-
4040
// Step through unversioned type to support v1beta1 -> v1
4141
unversioned := &apiextensions.CustomResourceDefinition{}
4242
if err := scheme.Convert(u, unversioned, nil); err != nil {
@@ -51,18 +51,74 @@ func Serialize(manifest string) (*apiextensionsv1.CustomResourceDefinition, erro
5151
// set CRD type meta
5252
// for purposes of fake client for unit tests to pass
5353
crd.TypeMeta.Kind = Kind
54-
crd.TypeMeta.APIVersion = APIVersion
55-
54+
crd.TypeMeta.APIVersion = Group + V1Version
5655

5756
// for each version in the CRD, check and make sure there is a schema
5857
// if not a schema, give a default schema of props
5958
for i := range crd.Spec.Versions {
6059
if crd.Spec.Versions[i].Schema == nil {
6160
schema := &apiextensionsv1.JSONSchemaProps{Type: "object"}
62-
crd.Spec.Versions[i].Schema = &apiextensionsv1.CustomResourceValidation{OpenAPIV3Schema:schema}
61+
crd.Spec.Versions[i].Schema = &apiextensionsv1.CustomResourceValidation{OpenAPIV3Schema: schema}
6362
}
6463
}
6564

65+
return crd, nil
66+
}
67+
68+
// SerializeV1 takes in a CRD manifest and returns a v1 versioned CRD object.
69+
// Compatible with v1beta1 or v1 CRD manifests.
70+
func SerializeV1Beta1(manifest string) (*apiextensionsv1beta1.CustomResourceDefinition, error) {
71+
u := &unstructured.Unstructured{}
72+
reader := bytes.NewReader([]byte(manifest))
73+
decoder := yaml.NewYAMLOrJSONDecoder(reader, 30)
74+
if err := decoder.Decode(u); err != nil {
75+
return nil, fmt.Errorf("crd unmarshaling failed: %s", err)
76+
}
77+
78+
unversioned := &apiextensions.CustomResourceDefinition{}
79+
if err := scheme.Convert(u, unversioned, nil); err != nil {
80+
return nil, fmt.Errorf("failed to convert crd from unstructured to internal: %s\nto v1: %s", u, err)
81+
}
82+
83+
crd := &apiextensionsv1beta1.CustomResourceDefinition{}
84+
if err := scheme.Convert(unversioned, crd, nil); err != nil {
85+
return nil, fmt.Errorf("failed to convert crd from internal to v1: %s\nto v1: %s", u, err)
86+
}
87+
88+
// set CRD type meta
89+
// for purposes of fake client for unit tests to pass
90+
crd.TypeMeta.Kind = Kind
91+
crd.TypeMeta.APIVersion = Group + V1Beta1Version
92+
93+
// for each version in the CRD, check and make sure there is a schema
94+
// if not a schema, give a default schema of props
95+
// kube 1.18 requires a schema for all CRDs regardless of version
96+
for i := range crd.Spec.Versions {
97+
if crd.Spec.Versions[i].Schema == nil {
98+
schema := &apiextensionsv1beta1.JSONSchemaProps{Type: "object"}
99+
crd.Spec.Versions[i].Schema = &apiextensionsv1beta1.CustomResourceValidation{OpenAPIV3Schema: schema}
100+
}
101+
}
66102

67103
return crd, nil
68104
}
105+
106+
// SerializeV1FromExisting takes in either a v1beta1 CRD or a v1 CRD type and returns a v1 CRD type.
107+
func SerializeV1FromExisting(crd interface{}) (*apiextensionsv1.CustomResourceDefinition, error) {
108+
if c, ok := crd.(*apiextensionsv1.CustomResourceDefinition); ok {
109+
return c, nil
110+
}
111+
if c, ok := crd.(*apiextensionsv1beta1.CustomResourceDefinition); ok {
112+
unversioned := &apiextensions.CustomResourceDefinition{}
113+
if err := scheme.Convert(c, unversioned, nil); err != nil {
114+
return nil, fmt.Errorf("failed to convert crd from unstructured to internal: %s\nto v1: %s", c.String(), err)
115+
}
116+
117+
c := &apiextensionsv1.CustomResourceDefinition{}
118+
if err := scheme.Convert(unversioned, crd, nil); err != nil {
119+
return nil, fmt.Errorf("failed to convert crd from internal to v1: %s\nto v1: %s", c.String(), err)
120+
}
121+
return c, nil
122+
}
123+
return nil, fmt.Errorf("unable to determine crd type")
124+
}

pkg/lib/crd/version.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,24 @@ var supportedCRDVersions = map[string]struct{}{
2323

2424
// Version takes a CRD manifest and determines whether it is v1 or v1beta1 type based on the APIVersion.
2525
func Version(manifest *string) (string, error) {
26+
if manifest == nil {
27+
return "", fmt.Errorf("empty CRD manifest")
28+
}
29+
2630
dec := yaml.NewYAMLOrJSONDecoder(strings.NewReader(*manifest), 10)
2731
unst := &unstructured.Unstructured{}
2832
if err := dec.Decode(unst); err != nil {
2933
return "", err
3034
}
3135

3236
v := unst.GetObjectKind().GroupVersionKind().Version
37+
// some e2e test fixtures do not provide an API version in their typemeta
38+
// assume these are v1beta types
39+
if v == "" {
40+
v = V1Beta1Version
41+
}
3342
if _, ok := supportedCRDVersions[v]; !ok {
34-
return "", fmt.Errorf("could not determine CRD version from manifest")
43+
return "", fmt.Errorf("CRD APIVersion from manifest not supported: %s", v)
3544
}
3645

3746
return v, nil

0 commit comments

Comments
 (0)