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  }