github.com/crossplane/upjet@v1.3.0/pkg/migration/patches.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 "log" 10 "reflect" 11 "regexp" 12 "strings" 13 14 xpv1 "github.com/crossplane/crossplane/apis/apiextensions/v1" 15 "github.com/pkg/errors" 16 "k8s.io/apimachinery/pkg/runtime" 17 "k8s.io/apimachinery/pkg/runtime/schema" 18 ) 19 20 var ( 21 regexIndex = regexp.MustCompile(`(.+)\[(.+)]`) 22 regexJSONTag = regexp.MustCompile(`([^,]+)?(,.+)?`) 23 ) 24 25 const ( 26 jsonTagInlined = ",inline" 27 ) 28 29 // isConverted looks up the specified name in the list of already converted 30 // patch sets. 31 func isConverted(convertedPS []string, psName string) bool { 32 for _, n := range convertedPS { 33 if psName == n { 34 return true 35 } 36 } 37 return false 38 } 39 40 // removeInvalidPatches removes the (inherited) patches from 41 // a (split) migration target composed template. The migration target composed 42 // templates inherit patches from migration source templates by default, and 43 // this function is responsible for removing patches (including references to 44 // patch sets) that do not conform to the target composed template's schema. 45 func (pg *PlanGenerator) removeInvalidPatches(gvkSource, gvkTarget schema.GroupVersionKind, patchSets []xpv1.PatchSet, targetTemplate *xpv1.ComposedTemplate, convertedPS []string) error { //nolint:gocyclo // complexity (11) just above the threshold (10) 46 c := pg.registry.scheme 47 source, err := c.New(gvkSource) 48 if err != nil { 49 return errors.Wrapf(err, "failed to instantiate a new source object with GVK: %s", gvkSource.String()) 50 } 51 target, err := c.New(gvkTarget) 52 if err != nil { 53 return errors.Wrapf(err, "failed to instantiate a new target object with GVK: %s", gvkTarget.String()) 54 } 55 56 newPatches := make([]xpv1.Patch, 0, len(targetTemplate.Patches)) 57 var patches []xpv1.Patch 58 for _, p := range targetTemplate.Patches { 59 s := source 60 switch p.Type { //nolint:exhaustive 61 case xpv1.PatchTypePatchSet: 62 ps := getNamedPatchSet(p.PatchSetName, patchSets) 63 if ps == nil { 64 // something is wrong with the patchset ref, 65 // we will just remove the ref 66 continue 67 } 68 if isConverted(convertedPS, ps.Name) { 69 // then do not use the source schema as the patch set 70 // is already converted 71 s = target 72 } 73 // assert each of the patches in the set 74 // conform the target schema 75 patches = ps.Patches 76 default: 77 patches = []xpv1.Patch{p} 78 } 79 keep := true 80 for _, p := range patches { 81 ok, err := assertPatchSchemaConformance(p, s, target) 82 if err != nil { 83 err := errors.Wrap(err, "failed to check whether the patch conforms to the target schema") 84 if pg.ErrorOnInvalidPatchSchema { 85 return err 86 } 87 log.Printf("Excluding the patch from the migration target because conformance checking has failed with: %v\n", err) 88 // if we could not check the patch's schema conformance 89 // and the plan generator is configured not to error, 90 // assume the patch does not conform to the schema 91 ok = false 92 } 93 if !ok { 94 keep = false 95 break 96 } 97 } 98 if keep { 99 newPatches = append(newPatches, p) 100 } 101 } 102 targetTemplate.Patches = newPatches 103 return nil 104 } 105 106 // assertPatchSchemaConformance asserts that the specified patch actually 107 // conforms the specified target schema. We also assert the patch conforms 108 // to the migration source schema, which prevents an invalid patch from being 109 // preserved after the conversion. 110 func assertPatchSchemaConformance(p xpv1.Patch, source, target any) (bool, error) { 111 var targetPath *string 112 // because this is defaulting logic and what we default can be overridden 113 // later in the convert, the type switch is not exhaustive 114 // TODO: consider processing other patch types 115 switch p.Type { //nolint:exhaustive 116 case xpv1.PatchTypeFromCompositeFieldPath, "": // the default type 117 targetPath = p.ToFieldPath 118 case xpv1.PatchTypeToCompositeFieldPath: 119 targetPath = p.FromFieldPath 120 } 121 if targetPath == nil { 122 return false, nil 123 } 124 ok, err := assertNameAndTypeAtPath(reflect.TypeOf(source), reflect.TypeOf(target), splitPathComponents(*targetPath)) 125 return ok, errors.Wrapf(err, "failed to assert patch schema for path: %s", *targetPath) 126 } 127 128 // splitPathComponents splits a fieldpath expression into its path components, 129 // e.g., `m[a.b.c].a.b.c` is split into `m[a.b.c]`, `a`, `b`, `c`. 130 func splitPathComponents(path string) []string { 131 components := strings.Split(path, ".") 132 result := make([]string, 0, len(components)) 133 indexedExpression := false 134 for _, c := range components { 135 switch { 136 case indexedExpression: 137 result[len(result)-1] = fmt.Sprintf("%s.%s", result[len(result)-1], c) 138 if strings.Contains(c, "]") { 139 indexedExpression = false 140 } 141 default: 142 result = append(result, c) 143 if strings.Contains(c, "[") && !strings.Contains(c, "]") { 144 indexedExpression = true 145 } 146 } 147 } 148 return result 149 } 150 151 func isRawExtension(source, target reflect.Type) bool { 152 reType := reflect.TypeOf(runtime.RawExtension{}) 153 rePtrType := reflect.TypeOf(&runtime.RawExtension{}) 154 return (source == reType && target == reType) || (source == rePtrType && target == rePtrType) 155 } 156 157 // assertNameAndTypeAtPath asserts that the migration source and target 158 // templates both have the same kind for the type at the specified path. 159 // Also validates the specific path is valid for the source. 160 func assertNameAndTypeAtPath(source, target reflect.Type, pathComponents []string) (bool, error) { //nolint:gocyclo 161 if len(pathComponents) < 1 { 162 return compareKinds(source, target), nil 163 } 164 // if both source and target are runtime.RawExtensions, 165 // then stop traversing the type hierarchy. 166 if isRawExtension(source, target) { 167 return true, nil 168 } 169 170 pathComponent := pathComponents[0] 171 if len(pathComponent) == 0 { 172 return false, errors.Errorf("failed to compare source and target structs. Invalid path: %s", strings.Join(pathComponents, ".")) 173 } 174 m := regexIndex.FindStringSubmatch(pathComponent) 175 if m != nil { 176 // then a map component or a slicing component 177 pathComponent = m[1] 178 } 179 180 // assert the source and the target types 181 fSource, err := getFieldWithSerializedName(source, pathComponent) 182 if err != nil { 183 return false, errors.Wrapf(err, "failed to assert source struct field kind at path: %s", strings.Join(pathComponents, ".")) 184 } 185 if fSource == nil { 186 // then source field could not be found 187 return false, errors.Errorf("struct field %q does not exist for the source type %q at path: %s", pathComponent, source.String(), strings.Join(pathComponents, ".")) 188 } 189 // now assert that this field actually exists for the target type 190 // with the same type 191 fTarget, err := getFieldWithSerializedName(target, pathComponent) 192 if err != nil { 193 return false, errors.Wrapf(err, "failed to assert target struct field kind at path: %s", strings.Join(pathComponents, ".")) 194 } 195 if fTarget == nil || !fTarget.IsExported() || !compareKinds(fSource.Type, fTarget.Type) { 196 return false, nil 197 } 198 199 nextSource, nextTarget := fSource.Type, fTarget.Type 200 if m != nil { 201 // parents are of map or slice type 202 nextSource = nextSource.Elem() 203 nextTarget = nextTarget.Elem() 204 } 205 return assertNameAndTypeAtPath(nextSource, nextTarget, pathComponents[1:]) 206 } 207 208 // compareKinds compares the kinds of the specified types 209 // dereferencing (following) pointer types. 210 func compareKinds(s, t reflect.Type) bool { 211 if s.Kind() == reflect.Pointer { 212 s = s.Elem() 213 } 214 if t.Kind() == reflect.Pointer { 215 t = t.Elem() 216 } 217 return s.Kind() == t.Kind() 218 } 219 220 // getFieldWithSerializedName returns the field of a struct (if it exists) 221 // with the specified serialized (JSON) name. Returns a nil (and a nil error) 222 // if a field with the specified serialized name is not found 223 // in the specified type. 224 func getFieldWithSerializedName(t reflect.Type, name string) (*reflect.StructField, error) { //nolint:gocyclo 225 if t.Kind() == reflect.Pointer { 226 t = t.Elem() 227 } 228 if t.Kind() != reflect.Struct { 229 return nil, errors.Errorf("type is not a struct: %s", t.Name()) 230 } 231 for i := 0; i < t.NumField(); i++ { 232 f := t.Field(i) 233 serializedName := f.Name 234 inlined := false 235 if fTag, ok := f.Tag.Lookup("json"); ok { 236 if m := regexJSONTag.FindStringSubmatch(fTag); m != nil && len(m[1]) > 0 { 237 serializedName = m[1] 238 } 239 if strings.HasSuffix(fTag, jsonTagInlined) { 240 inlined = true 241 } 242 } 243 if name == serializedName { 244 return &f, nil 245 } 246 if inlined { 247 inlinedType := f.Type 248 if inlinedType.Kind() == reflect.Pointer { 249 inlinedType = inlinedType.Elem() 250 } 251 if inlinedType.Kind() == reflect.Struct { 252 sf, err := getFieldWithSerializedName(inlinedType, name) 253 if err != nil { 254 return nil, errors.Wrapf(err, "failed to search for field %q in inlined type: %s", name, inlinedType.String()) 255 } 256 if sf != nil { 257 return sf, nil 258 } 259 } 260 } 261 } 262 return nil, nil // not found 263 } 264 265 // getNamedPatchSet returns the patch set with the specified name 266 // from the specified patch set slice. Returns nil if a patch set 267 // with the given name is not found. 268 func getNamedPatchSet(name *string, patchSets []xpv1.PatchSet) *xpv1.PatchSet { 269 if name == nil { 270 // if name is not specified, do not attempt to find a named patchset 271 return nil 272 } 273 for _, ps := range patchSets { 274 if *name == ps.Name { 275 return &ps 276 } 277 } 278 return nil 279 } 280 281 // getConvertedPatchSetNames returns the names of patch sets that have been 282 // converted by a PatchSetConverter. 283 func getConvertedPatchSetNames(newPatchSets, oldPatchSets []xpv1.PatchSet) []string { 284 converted := make([]string, 0, len(newPatchSets)) 285 for _, n := range newPatchSets { 286 found := false 287 for _, o := range oldPatchSets { 288 if o.Name != n.Name { 289 continue 290 } 291 found = true 292 if !reflect.DeepEqual(o, n) { 293 converted = append(converted, n.Name) 294 } 295 break 296 } 297 if !found { 298 converted = append(converted, n.Name) 299 } 300 } 301 return converted 302 } 303 304 // convertToMap converts the given slice of patch sets to a map of 305 // patch sets keyed by their names. 306 func convertToMap(ps []xpv1.PatchSet) map[string]*xpv1.PatchSet { 307 m := make(map[string]*xpv1.PatchSet, len(ps)) 308 for _, p := range ps { 309 // Crossplane dereferences the last patch set with the same name, 310 // so override with the last patch set with the same name. 311 m[p.Name] = p.DeepCopy() 312 } 313 return m 314 } 315 316 // convertFromMap converts the specified map of patch sets back to a slice. 317 // If filterDeleted is set, previously existing patch sets in the Composition 318 // which have been removed from the map are also removed from the resulting 319 // slice, and eventually from the Composition. PatchSetConverters are 320 // allowed to remove patch sets, whereas Composition converters are 321 // not, as Composition converters have a local view of the patch sets and 322 // don't know about the other composed templates that may be sharing 323 // patch sets with them. 324 func convertFromMap(psMap map[string]*xpv1.PatchSet, oldPS []xpv1.PatchSet, filterDeleted bool) []xpv1.PatchSet { 325 result := make([]xpv1.PatchSet, 0, len(psMap)) 326 for _, ps := range oldPS { 327 if filterDeleted && psMap[ps.Name] == nil { 328 // then patch set has been deleted 329 continue 330 } 331 if psMap[ps.Name] == nil { 332 result = append(result, ps) 333 continue 334 } 335 result = append(result, *psMap[ps.Name]) 336 delete(psMap, ps.Name) 337 } 338 // add the new patch sets 339 for _, ps := range psMap { 340 if ps == nil { 341 continue 342 } 343 result = append(result, *ps) 344 } 345 return result 346 }