sigs.k8s.io/cluster-api@v1.7.1/util/conversion/conversion.go (about) 1 /* 2 Copyright 2019 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 conversion implements conversion utilities. 18 package conversion 19 20 import ( 21 "context" 22 "math/rand" 23 "sort" 24 "strings" 25 "testing" 26 27 "github.com/google/go-cmp/cmp" 28 fuzz "github.com/google/gofuzz" 29 "github.com/onsi/gomega" 30 "github.com/pkg/errors" 31 corev1 "k8s.io/api/core/v1" 32 "k8s.io/apimachinery/pkg/api/apitesting/fuzzer" 33 apiequality "k8s.io/apimachinery/pkg/api/equality" 34 metafuzzer "k8s.io/apimachinery/pkg/apis/meta/fuzzer" 35 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 36 "k8s.io/apimachinery/pkg/runtime" 37 runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer" 38 "k8s.io/apimachinery/pkg/util/json" 39 "k8s.io/client-go/kubernetes/scheme" 40 "sigs.k8s.io/controller-runtime/pkg/client" 41 "sigs.k8s.io/controller-runtime/pkg/conversion" 42 43 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 44 "sigs.k8s.io/cluster-api/util" 45 ) 46 47 const ( 48 // DataAnnotation is the annotation that conversion webhooks 49 // use to retain the data in case of down-conversion from the hub. 50 DataAnnotation = "cluster.x-k8s.io/conversion-data" 51 ) 52 53 var ( 54 contract = clusterv1.GroupVersion.String() 55 ) 56 57 // UpdateReferenceAPIContract takes a client and object reference, queries the API Server for 58 // the Custom Resource Definition and looks which one is the stored version available. 59 // 60 // The object passed as input is modified in place if an updated compatible version is found. 61 // NOTE: This version depends on CRDs being named correctly as defined by contract.CalculateCRDName. 62 func UpdateReferenceAPIContract(ctx context.Context, c client.Client, ref *corev1.ObjectReference) error { 63 gvk := ref.GroupVersionKind() 64 65 metadata, err := util.GetGVKMetadata(ctx, c, gvk) 66 if err != nil { 67 return errors.Wrapf(err, "failed to update apiVersion in ref") 68 } 69 70 chosen, err := getLatestAPIVersionFromContract(metadata) 71 if err != nil { 72 return errors.Wrapf(err, "failed to update apiVersion in ref") 73 } 74 75 // Modify the GroupVersionKind with the new version. 76 if gvk.Version != chosen { 77 gvk.Version = chosen 78 ref.SetGroupVersionKind(gvk) 79 } 80 81 return nil 82 } 83 84 func getLatestAPIVersionFromContract(metadata metav1.Object) (string, error) { 85 labels := metadata.GetLabels() 86 87 // If there is no label, return early without changing the reference. 88 supportedVersions, ok := labels[contract] 89 if !ok || supportedVersions == "" { 90 return "", errors.Errorf("cannot find any versions matching contract %q for CRD %v as contract version label(s) are either missing or empty (see https://cluster-api.sigs.k8s.io/developer/providers/contracts.html#api-version-labels)", contract, metadata.GetName()) 91 } 92 93 // Pick the latest version in the slice and validate it. 94 kubeVersions := util.KubeAwareAPIVersions(strings.Split(supportedVersions, "_")) 95 sort.Sort(kubeVersions) 96 return kubeVersions[len(kubeVersions)-1], nil 97 } 98 99 // MarshalData stores the source object as json data in the destination object annotations map. 100 // It ignores the metadata of the source object. 101 func MarshalData(src metav1.Object, dst metav1.Object) error { 102 u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(src) 103 if err != nil { 104 return err 105 } 106 delete(u, "metadata") 107 108 data, err := json.Marshal(u) 109 if err != nil { 110 return err 111 } 112 annotations := dst.GetAnnotations() 113 if annotations == nil { 114 annotations = map[string]string{} 115 } 116 annotations[DataAnnotation] = string(data) 117 dst.SetAnnotations(annotations) 118 return nil 119 } 120 121 // UnmarshalData tries to retrieve the data from the annotation and unmarshals it into the object passed as input. 122 func UnmarshalData(from metav1.Object, to interface{}) (bool, error) { 123 annotations := from.GetAnnotations() 124 data, ok := annotations[DataAnnotation] 125 if !ok { 126 return false, nil 127 } 128 if err := json.Unmarshal([]byte(data), to); err != nil { 129 return false, err 130 } 131 delete(annotations, DataAnnotation) 132 from.SetAnnotations(annotations) 133 return true, nil 134 } 135 136 // GetFuzzer returns a new fuzzer to be used for testing. 137 func GetFuzzer(scheme *runtime.Scheme, funcs ...fuzzer.FuzzerFuncs) *fuzz.Fuzzer { 138 funcs = append([]fuzzer.FuzzerFuncs{ 139 metafuzzer.Funcs, 140 func(_ runtimeserializer.CodecFactory) []interface{} { 141 return []interface{}{ 142 // Custom fuzzer for metav1.Time pointers which weren't 143 // fuzzed and always resulted in `nil` values. 144 // This implementation is somewhat similar to the one provided 145 // in the metafuzzer.Funcs. 146 func(input *metav1.Time, c fuzz.Continue) { 147 if input != nil { 148 var sec, nsec uint32 149 c.Fuzz(&sec) 150 c.Fuzz(&nsec) 151 fuzzed := metav1.Unix(int64(sec), int64(nsec)).Rfc3339Copy() 152 input.Time = fuzzed.Time 153 } 154 }, 155 } 156 }, 157 }, funcs...) 158 return fuzzer.FuzzerFor( 159 fuzzer.MergeFuzzerFuncs(funcs...), 160 rand.NewSource(rand.Int63()), //nolint:gosec 161 runtimeserializer.NewCodecFactory(scheme), 162 ) 163 } 164 165 // FuzzTestFuncInput contains input parameters 166 // for the FuzzTestFunc function. 167 type FuzzTestFuncInput struct { 168 Scheme *runtime.Scheme 169 170 Hub conversion.Hub 171 HubAfterMutation func(conversion.Hub) 172 173 Spoke conversion.Convertible 174 SpokeAfterMutation func(convertible conversion.Convertible) 175 SkipSpokeAnnotationCleanup bool 176 177 FuzzerFuncs []fuzzer.FuzzerFuncs 178 } 179 180 // FuzzTestFunc returns a new testing function to be used in tests to make sure conversions between 181 // the Hub version of an object and an older version aren't lossy. 182 func FuzzTestFunc(input FuzzTestFuncInput) func(*testing.T) { 183 if input.Scheme == nil { 184 input.Scheme = scheme.Scheme 185 } 186 187 return func(t *testing.T) { 188 t.Helper() 189 t.Run("spoke-hub-spoke", func(t *testing.T) { 190 g := gomega.NewWithT(t) 191 fuzzer := GetFuzzer(input.Scheme, input.FuzzerFuncs...) 192 193 for i := 0; i < 10000; i++ { 194 // Create the spoke and fuzz it 195 spokeBefore := input.Spoke.DeepCopyObject().(conversion.Convertible) 196 fuzzer.Fuzz(spokeBefore) 197 198 // First convert spoke to hub 199 hubCopy := input.Hub.DeepCopyObject().(conversion.Hub) 200 g.Expect(spokeBefore.ConvertTo(hubCopy)).To(gomega.Succeed()) 201 202 // Convert hub back to spoke and check if the resulting spoke is equal to the spoke before the round trip 203 spokeAfter := input.Spoke.DeepCopyObject().(conversion.Convertible) 204 g.Expect(spokeAfter.ConvertFrom(hubCopy)).To(gomega.Succeed()) 205 206 // Remove data annotation eventually added by ConvertFrom for avoiding data loss in hub-spoke-hub round trips 207 // NOTE: There are use case when we want to skip this operation, e.g. if the spoke object does not have ObjectMeta (e.g. kubeadm types). 208 if !input.SkipSpokeAnnotationCleanup { 209 metaAfter := spokeAfter.(metav1.Object) 210 delete(metaAfter.GetAnnotations(), DataAnnotation) 211 } 212 213 if input.SpokeAfterMutation != nil { 214 input.SpokeAfterMutation(spokeAfter) 215 } 216 217 g.Expect(apiequality.Semantic.DeepEqual(spokeBefore, spokeAfter)).To(gomega.BeTrue(), cmp.Diff(spokeBefore, spokeAfter)) 218 } 219 }) 220 t.Run("hub-spoke-hub", func(t *testing.T) { 221 g := gomega.NewWithT(t) 222 fuzzer := GetFuzzer(input.Scheme, input.FuzzerFuncs...) 223 224 for i := 0; i < 10000; i++ { 225 // Create the hub and fuzz it 226 hubBefore := input.Hub.DeepCopyObject().(conversion.Hub) 227 fuzzer.Fuzz(hubBefore) 228 229 // First convert hub to spoke 230 dstCopy := input.Spoke.DeepCopyObject().(conversion.Convertible) 231 g.Expect(dstCopy.ConvertFrom(hubBefore)).To(gomega.Succeed()) 232 233 // Convert spoke back to hub and check if the resulting hub is equal to the hub before the round trip 234 hubAfter := input.Hub.DeepCopyObject().(conversion.Hub) 235 g.Expect(dstCopy.ConvertTo(hubAfter)).To(gomega.Succeed()) 236 237 if input.HubAfterMutation != nil { 238 input.HubAfterMutation(hubAfter) 239 } 240 241 g.Expect(apiequality.Semantic.DeepEqual(hubBefore, hubAfter)).To(gomega.BeTrue(), cmp.Diff(hubBefore, hubAfter)) 242 } 243 }) 244 } 245 }