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 }