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  }