github.com/crossplane/upjet@v1.3.0/pkg/migration/converter.go (about)

     1  // SPDX-FileCopyrightText: 2023 The Crossplane Authors <https://crossplane.io>
     2  //
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package migration
     6  
     7  import (
     8  	"fmt"
     9  
    10  	"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
    11  	xpmeta "github.com/crossplane/crossplane-runtime/pkg/meta"
    12  	"github.com/crossplane/crossplane-runtime/pkg/resource"
    13  	xpv1 "github.com/crossplane/crossplane/apis/apiextensions/v1"
    14  	xpmetav1 "github.com/crossplane/crossplane/apis/pkg/meta/v1"
    15  	xpmetav1alpha1 "github.com/crossplane/crossplane/apis/pkg/meta/v1alpha1"
    16  	xppkgv1 "github.com/crossplane/crossplane/apis/pkg/v1"
    17  	xppkgv1beta1 "github.com/crossplane/crossplane/apis/pkg/v1beta1"
    18  	"github.com/pkg/errors"
    19  	corev1 "k8s.io/api/core/v1"
    20  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    21  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    22  	"k8s.io/apimachinery/pkg/runtime"
    23  	"k8s.io/apimachinery/pkg/runtime/schema"
    24  	"k8s.io/apimachinery/pkg/util/json"
    25  	k8sjson "sigs.k8s.io/json"
    26  )
    27  
    28  const (
    29  	errFromUnstructured            = "failed to convert from unstructured.Unstructured to the managed resource type"
    30  	errFromUnstructuredConfMeta    = "failed to convert from unstructured.Unstructured to Crossplane Configuration metadata"
    31  	errFromUnstructuredConfPackage = "failed to convert from unstructured.Unstructured to Crossplane Configuration package"
    32  	errFromUnstructuredProvider    = "failed to convert from unstructured.Unstructured to Crossplane Provider package"
    33  	errFromUnstructuredLock        = "failed to convert from unstructured.Unstructured to Crossplane package lock"
    34  	errToUnstructured              = "failed to convert from the managed resource type to unstructured.Unstructured"
    35  	errRawExtensionUnmarshal       = "failed to unmarshal runtime.RawExtension"
    36  
    37  	errFmtPavedDelete = "failed to delete fieldpath %q from paved"
    38  
    39  	metadataAnnotationPaveKey = "metadata.annotations['%s']"
    40  )
    41  
    42  // CopyInto copies values of fields from the migration `source` object
    43  // into the migration `target` object and fills in the target object's
    44  // TypeMeta using the supplied `targetGVK`. While copying fields from
    45  // migration source to migration target, the fields at the paths
    46  // specified with `skipFieldPaths` array are skipped. This is a utility
    47  // that can be used in the migration resource converter implementations.
    48  // If a certain field with the same name in both the `source` and the `target`
    49  // objects has different types in `source` and `target`, then it must be
    50  // included in the `skipFieldPaths` and it must manually be handled in the
    51  // conversion function.
    52  func CopyInto(source any, target any, targetGVK schema.GroupVersionKind, skipFieldPaths ...string) (any, error) {
    53  	u := ToSanitizedUnstructured(source)
    54  	paved := fieldpath.Pave(u.Object)
    55  	skipFieldPaths = append(skipFieldPaths, "apiVersion", "kind",
    56  		fmt.Sprintf(metadataAnnotationPaveKey, xpmeta.AnnotationKeyExternalCreatePending),
    57  		fmt.Sprintf(metadataAnnotationPaveKey, xpmeta.AnnotationKeyExternalCreateSucceeded),
    58  		fmt.Sprintf(metadataAnnotationPaveKey, xpmeta.AnnotationKeyExternalCreateFailed),
    59  		fmt.Sprintf(metadataAnnotationPaveKey, corev1.LastAppliedConfigAnnotation),
    60  	)
    61  	for _, p := range skipFieldPaths {
    62  		if err := paved.DeleteField(p); err != nil {
    63  			return nil, errors.Wrapf(err, errFmtPavedDelete, p)
    64  		}
    65  	}
    66  	u.SetGroupVersionKind(targetGVK)
    67  	return target, errors.Wrap(runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, target), errFromUnstructured)
    68  }
    69  
    70  // sanitizeResource removes certain fields from the unstructured object.
    71  // It turns out that certain fields, such as `metadata.creationTimestamp`
    72  // are still serialized even if they have zero-values. This function
    73  // removes such fields. We also unconditionally sanitize `status`
    74  // so that the controller will populate it back.
    75  func sanitizeResource(m map[string]any) map[string]any {
    76  	delete(m, "status")
    77  	if _, ok := m["metadata"]; !ok {
    78  		return m
    79  	}
    80  	metadata := m["metadata"].(map[string]any)
    81  
    82  	if v := metadata["creationTimestamp"]; v == nil {
    83  		delete(metadata, "creationTimestamp")
    84  	}
    85  	if len(metadata) == 0 {
    86  		delete(m, "metadata")
    87  	}
    88  	removeNilValuedKeys(m)
    89  	return m
    90  }
    91  
    92  // removeNilValuedKeys removes nil values from the specified map so that
    93  // the serialized manifest do not contain corresponding superfluous YAML
    94  // nulls.
    95  func removeNilValuedKeys(m map[string]interface{}) {
    96  	for k, v := range m {
    97  		if v == nil {
    98  			delete(m, k)
    99  			continue
   100  		}
   101  		switch c := v.(type) {
   102  		case map[string]any:
   103  			removeNilValuedKeys(c)
   104  		case []any:
   105  			for _, e := range c {
   106  				if cm, ok := e.(map[string]interface{}); ok {
   107  					removeNilValuedKeys(cm)
   108  				}
   109  			}
   110  		}
   111  	}
   112  }
   113  
   114  // ToSanitizedUnstructured converts the specified managed resource to an
   115  // unstructured.Unstructured. Before the converted object is
   116  // returned, it's sanitized by removing certain fields
   117  // (like status, metadata.creationTimestamp).
   118  func ToSanitizedUnstructured(mg any) unstructured.Unstructured {
   119  	m, err := runtime.DefaultUnstructuredConverter.ToUnstructured(mg)
   120  	if err != nil {
   121  		panic(errors.Wrap(err, errToUnstructured))
   122  	}
   123  	return unstructured.Unstructured{
   124  		Object: sanitizeResource(m),
   125  	}
   126  }
   127  
   128  // FromRawExtension attempts to convert a runtime.RawExtension into
   129  // an unstructured.Unstructured.
   130  func FromRawExtension(r runtime.RawExtension) (unstructured.Unstructured, error) {
   131  	var m map[string]interface{}
   132  	if err := json.Unmarshal(r.Raw, &m); err != nil {
   133  		return unstructured.Unstructured{}, errors.Wrap(err, errRawExtensionUnmarshal)
   134  	}
   135  	return unstructured.Unstructured{
   136  		Object: m,
   137  	}, nil
   138  }
   139  
   140  // FromGroupVersionKind converts a schema.GroupVersionKind into
   141  // a migration.GroupVersionKind.
   142  func FromGroupVersionKind(gvk schema.GroupVersionKind) GroupVersionKind {
   143  	return GroupVersionKind{
   144  		Group:   gvk.Group,
   145  		Version: gvk.Version,
   146  		Kind:    gvk.Kind,
   147  	}
   148  }
   149  
   150  // ToComposition converts the specified unstructured.Unstructured to
   151  // a Crossplane Composition.
   152  // Workaround for:
   153  // https://github.com/kubernetes-sigs/structured-merge-diff/issues/230
   154  func ToComposition(u unstructured.Unstructured) (*xpv1.Composition, error) {
   155  	buff, err := json.Marshal(u.Object)
   156  	if err != nil {
   157  		return nil, errors.Wrap(err, "failed to marshal map to JSON")
   158  	}
   159  	c := &xpv1.Composition{}
   160  	return c, errors.Wrap(k8sjson.UnmarshalCaseSensitivePreserveInts(buff, c), "failed to unmarshal into a v1.Composition")
   161  }
   162  
   163  func addGVK(u unstructured.Unstructured, target map[string]any) map[string]any {
   164  	if target == nil {
   165  		target = make(map[string]any)
   166  	}
   167  	target["apiVersion"] = u.GetAPIVersion()
   168  	target["kind"] = u.GetKind()
   169  	return target
   170  }
   171  
   172  func addNameGVK(u unstructured.Unstructured, target map[string]any) map[string]any {
   173  	target = addGVK(u, target)
   174  	m := target["metadata"]
   175  	if m == nil {
   176  		m = make(map[string]any)
   177  	}
   178  	metadata := m.(map[string]any)
   179  	metadata["name"] = u.GetName()
   180  	if len(u.GetNamespace()) != 0 {
   181  		metadata["namespace"] = u.GetNamespace()
   182  	}
   183  	target["metadata"] = m
   184  	return target
   185  }
   186  
   187  func toManagedResource(c runtime.ObjectCreater, u unstructured.Unstructured) (resource.Managed, bool, error) {
   188  	gvk := u.GroupVersionKind()
   189  	if gvk == xpv1.CompositionGroupVersionKind {
   190  		return nil, false, nil
   191  	}
   192  	obj, err := c.New(gvk)
   193  	if err != nil {
   194  		return nil, false, errors.Wrapf(err, errFmtNewObject, gvk)
   195  	}
   196  	if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, obj); err != nil {
   197  		return nil, false, errors.Wrap(err, errFromUnstructured)
   198  	}
   199  	mg, ok := obj.(resource.Managed)
   200  	return mg, ok, nil
   201  }
   202  
   203  func toConfigurationPackageV1(u unstructured.Unstructured) (*xppkgv1.Configuration, error) {
   204  	conf := &xppkgv1.Configuration{}
   205  	if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, conf); err != nil {
   206  		return nil, errors.Wrap(err, errFromUnstructuredConfPackage)
   207  	}
   208  	return conf, nil
   209  }
   210  
   211  func toConfigurationMetadataV1(u unstructured.Unstructured) (*xpmetav1.Configuration, error) {
   212  	conf := &xpmetav1.Configuration{}
   213  	if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, conf); err != nil {
   214  		return nil, errors.Wrap(err, errFromUnstructuredConfMeta)
   215  	}
   216  	return conf, nil
   217  }
   218  
   219  func toConfigurationMetadataV1Alpha1(u unstructured.Unstructured) (*xpmetav1alpha1.Configuration, error) {
   220  	conf := &xpmetav1alpha1.Configuration{}
   221  	if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, conf); err != nil {
   222  		return nil, errors.Wrap(err, errFromUnstructuredConfMeta)
   223  	}
   224  	return conf, nil
   225  }
   226  
   227  func toConfigurationMetadata(u unstructured.Unstructured) (metav1.Object, error) {
   228  	var conf metav1.Object
   229  	var err error
   230  	switch u.GroupVersionKind().Version {
   231  	case "v1alpha1":
   232  		conf, err = toConfigurationMetadataV1Alpha1(u)
   233  	default:
   234  		conf, err = toConfigurationMetadataV1(u)
   235  	}
   236  	return conf, err
   237  }
   238  
   239  func toProviderPackage(u unstructured.Unstructured) (*xppkgv1.Provider, error) {
   240  	pkg := &xppkgv1.Provider{}
   241  	if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, pkg); err != nil {
   242  		return nil, errors.Wrap(err, errFromUnstructuredProvider)
   243  	}
   244  	return pkg, nil
   245  }
   246  
   247  func getCategory(u unstructured.Unstructured) Category {
   248  	switch u.GroupVersionKind() {
   249  	case xpv1.CompositionGroupVersionKind:
   250  		return CategoryComposition
   251  	default:
   252  		return categoryUnknown
   253  	}
   254  }
   255  
   256  func toPackageLock(u unstructured.Unstructured) (*xppkgv1beta1.Lock, error) {
   257  	lock := &xppkgv1beta1.Lock{}
   258  	if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, lock); err != nil {
   259  		return nil, errors.Wrap(err, errFromUnstructuredLock)
   260  	}
   261  	return lock, nil
   262  }
   263  
   264  // ConvertComposedTemplatePatchesMap converts the composed templates with given conversionMap
   265  // Key of the conversionMap points to the source field
   266  // Value of the conversionMap points to the target field
   267  func ConvertComposedTemplatePatchesMap(sourceTemplate xpv1.ComposedTemplate, conversionMap map[string]string) []xpv1.Patch {
   268  	var patchesToAdd []xpv1.Patch
   269  	for _, p := range sourceTemplate.Patches {
   270  		switch p.Type { //nolint:exhaustive
   271  		case xpv1.PatchTypeFromCompositeFieldPath, xpv1.PatchTypeCombineFromComposite, xpv1.PatchTypeFromEnvironmentFieldPath, xpv1.PatchTypeCombineFromEnvironment, "":
   272  			{
   273  				if p.ToFieldPath != nil {
   274  					if to, ok := conversionMap[*p.ToFieldPath]; ok {
   275  						patchesToAdd = append(patchesToAdd, xpv1.Patch{
   276  							Type:          p.Type,
   277  							FromFieldPath: p.FromFieldPath,
   278  							ToFieldPath:   &to,
   279  							Transforms:    p.Transforms,
   280  							Policy:        p.Policy,
   281  							Combine:       p.Combine,
   282  						})
   283  					}
   284  				}
   285  			}
   286  		case xpv1.PatchTypeToCompositeFieldPath, xpv1.PatchTypeCombineToComposite, xpv1.PatchTypeToEnvironmentFieldPath, xpv1.PatchTypeCombineToEnvironment:
   287  			{
   288  				if p.FromFieldPath != nil {
   289  					if to, ok := conversionMap[*p.FromFieldPath]; ok {
   290  						patchesToAdd = append(patchesToAdd, xpv1.Patch{
   291  							Type:          p.Type,
   292  							FromFieldPath: &to,
   293  							ToFieldPath:   p.ToFieldPath,
   294  							Transforms:    p.Transforms,
   295  							Policy:        p.Policy,
   296  							Combine:       p.Combine,
   297  						})
   298  					}
   299  				}
   300  			}
   301  		}
   302  	}
   303  	return patchesToAdd
   304  }