github.com/crossplane/upjet@v1.3.0/pkg/migration/plan_generator.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  	"reflect"
    10  	"time"
    11  
    12  	"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
    13  	"github.com/crossplane/crossplane-runtime/pkg/resource"
    14  	xpv1 "github.com/crossplane/crossplane/apis/apiextensions/v1"
    15  	xpmetav1 "github.com/crossplane/crossplane/apis/pkg/meta/v1"
    16  	xpmetav1alpha1 "github.com/crossplane/crossplane/apis/pkg/meta/v1alpha1"
    17  	xppkgv1 "github.com/crossplane/crossplane/apis/pkg/v1"
    18  	xppkgv1beta1 "github.com/crossplane/crossplane/apis/pkg/v1beta1"
    19  	"github.com/pkg/errors"
    20  	corev1 "k8s.io/api/core/v1"
    21  	"k8s.io/apimachinery/pkg/runtime"
    22  	"k8s.io/apimachinery/pkg/runtime/schema"
    23  	"k8s.io/apimachinery/pkg/util/rand"
    24  )
    25  
    26  const (
    27  	errSourceHasNext                   = "failed to generate migration plan: Could not check next object from source"
    28  	errSourceNext                      = "failed to generate migration plan: Could not get next object from source"
    29  	errPreProcessFmt                   = "failed to pre-process the manifest of category %q"
    30  	errSourceReset                     = "failed to generate migration plan: Could not get reset the source"
    31  	errUnstructuredConvert             = "failed to convert from unstructured object to v1.Composition"
    32  	errUnstructuredMarshal             = "failed to marshal unstructured object to JSON"
    33  	errResourceMigrate                 = "failed to migrate resource"
    34  	errCompositePause                  = "failed to pause composite resource"
    35  	errCompositesEdit                  = "failed to edit composite resources"
    36  	errCompositesStart                 = "failed to start composite resources"
    37  	errCompositionMigrateFmt           = "failed to migrate the composition: %s"
    38  	errConfigurationMetadataMigrateFmt = "failed to migrate the configuration metadata: %s"
    39  	errConfigurationPackageMigrateFmt  = "failed to migrate the configuration package: %s"
    40  	errProviderMigrateFmt              = "failed to migrate the Provider package: %s"
    41  	errLockMigrateFmt                  = "failed to migrate the package lock: %s"
    42  	errComposedTemplateBase            = "failed to migrate the base of a composed template"
    43  	errComposedTemplateMigrate         = "failed to migrate the composed templates of the composition"
    44  	errResourceOutput                  = "failed to output migrated resource"
    45  	errResourceOrphan                  = "failed to orphan managed resource"
    46  	errResourceRemoveFinalizer         = "failed to remove finalizers of managed resource"
    47  	errCompositionOutput               = "failed to output migrated composition"
    48  	errCompositeOutput                 = "failed to output migrated composite"
    49  	errClaimOutput                     = "failed to output migrated claim"
    50  	errClaimsEdit                      = "failed to edit claims"
    51  	errPlanGeneration                  = "failed to generate the migration plan"
    52  	errPause                           = "failed to store a paused manifest"
    53  	errMissingGVK                      = "managed resource is missing its GVK. Resource converters must set GVKs on any managed resources they newly generate."
    54  )
    55  
    56  const (
    57  	versionV010 = "0.1.0"
    58  
    59  	keyCompositionRef = "compositionRef"
    60  	keyResourceRefs   = "resourceRefs"
    61  )
    62  
    63  // PlanGeneratorOption configures a PlanGenerator
    64  type PlanGeneratorOption func(generator *PlanGenerator)
    65  
    66  // WithErrorOnInvalidPatchSchema returns a PlanGeneratorOption for configuring
    67  // whether the PlanGenerator should error and stop the migration plan
    68  // generation in case an error is encountered while checking a patch
    69  // statement's conformance to the migration source or target.
    70  func WithErrorOnInvalidPatchSchema(e bool) PlanGeneratorOption {
    71  	return func(pg *PlanGenerator) {
    72  		pg.ErrorOnInvalidPatchSchema = e
    73  	}
    74  }
    75  
    76  // WithSkipGVKs configures the set of GVKs to skip for conversion
    77  // during a migration.
    78  func WithSkipGVKs(gvk ...schema.GroupVersionKind) PlanGeneratorOption {
    79  	return func(pg *PlanGenerator) {
    80  		pg.SkipGVKs = gvk
    81  	}
    82  }
    83  
    84  // WithMultipleSources can be used to configure multiple sources for a
    85  // PlanGenerator.
    86  func WithMultipleSources(source ...Source) PlanGeneratorOption {
    87  	return func(pg *PlanGenerator) {
    88  		pg.source = &sources{backends: source}
    89  	}
    90  }
    91  
    92  // WithEnableConfigurationMigrationSteps enables only
    93  // the configuration migration steps.
    94  // TODO: to be replaced with a higher abstraction encapsulating
    95  // migration scenarios.
    96  func WithEnableConfigurationMigrationSteps() PlanGeneratorOption {
    97  	return func(pg *PlanGenerator) {
    98  		pg.enabledSteps = getConfigurationMigrationSteps()
    99  	}
   100  }
   101  
   102  func WithEnableOnlyFileSystemAPISteps() PlanGeneratorOption {
   103  	return func(pg *PlanGenerator) {
   104  		pg.enabledSteps = getAPIMigrationStepsFileSystemMode()
   105  	}
   106  }
   107  
   108  type sources struct {
   109  	backends []Source
   110  	i        int
   111  }
   112  
   113  func (s *sources) HasNext() (bool, error) {
   114  	if s.i >= len(s.backends) {
   115  		return false, nil
   116  	}
   117  	ok, err := s.backends[s.i].HasNext()
   118  	if err != nil || ok {
   119  		return ok, err
   120  	}
   121  	s.i++
   122  	return s.HasNext()
   123  }
   124  
   125  func (s *sources) Next() (UnstructuredWithMetadata, error) {
   126  	return s.backends[s.i].Next()
   127  }
   128  
   129  func (s *sources) Reset() error {
   130  	for _, src := range s.backends {
   131  		if err := src.Reset(); err != nil {
   132  			return err
   133  		}
   134  	}
   135  	s.i = 0
   136  	return nil
   137  }
   138  
   139  // PlanGenerator generates a migration.Plan reading the manifests available
   140  // from `source`, converting managed resources and compositions using the
   141  // available `migration.Converter`s registered in the `registry` and
   142  // writing the output manifests to the specified `target`.
   143  type PlanGenerator struct {
   144  	source       Source
   145  	target       Target
   146  	registry     *Registry
   147  	subSteps     map[step]string
   148  	enabledSteps []step
   149  	// Plan is the migration.Plan whose steps are expected
   150  	// to complete a migration when they're executed in order.
   151  	Plan Plan
   152  	// ErrorOnInvalidPatchSchema errors and stops plan generation in case
   153  	// an error is encountered while checking the conformance of a patch
   154  	// statement against the migration source or the migration target.
   155  	ErrorOnInvalidPatchSchema bool
   156  	// GVKs of managed resources that
   157  	// should be skipped for conversion during the migration, if no
   158  	// converters are registered for them. If any of the GVK components
   159  	// is left empty, it will be a wildcard component.
   160  	// Exact matching with an empty group name is not possible.
   161  	SkipGVKs []schema.GroupVersionKind
   162  }
   163  
   164  // NewPlanGenerator constructs a new PlanGenerator using the specified
   165  // Source and Target and the default converter Registry.
   166  func NewPlanGenerator(registry *Registry, source Source, target Target, opts ...PlanGeneratorOption) PlanGenerator {
   167  	pg := &PlanGenerator{
   168  		source:       &sources{backends: []Source{source}},
   169  		target:       target,
   170  		registry:     registry,
   171  		subSteps:     map[step]string{},
   172  		enabledSteps: getAPIMigrationSteps(),
   173  	}
   174  	for _, o := range opts {
   175  		o(pg)
   176  	}
   177  	return *pg
   178  }
   179  
   180  // GeneratePlan generates a migration plan for the manifests available from
   181  // the configured Source and writing them to the configured Target using the
   182  // configured converter Registry. The generated Plan is available in the
   183  // PlanGenerator.Plan variable if the generation is successful
   184  // (i.e., no errors are reported).
   185  func (pg *PlanGenerator) GeneratePlan() error {
   186  	pg.Plan.Spec.stepMap = make(map[string]*Step)
   187  	pg.Plan.Version = versionV010
   188  	defer pg.commitSteps()
   189  	if err := pg.preProcess(); err != nil {
   190  		return err
   191  	}
   192  	if err := pg.source.Reset(); err != nil {
   193  		return errors.Wrap(err, errSourceReset)
   194  	}
   195  	return errors.Wrap(pg.convert(), errPlanGeneration)
   196  }
   197  
   198  func (pg *PlanGenerator) preProcess() error {
   199  	if len(pg.registry.unstructuredPreProcessors) == 0 {
   200  		return nil
   201  	}
   202  	for hasNext, err := pg.source.HasNext(); ; hasNext, err = pg.source.HasNext() {
   203  		if err != nil {
   204  			return errors.Wrap(err, errSourceHasNext)
   205  		}
   206  		if !hasNext {
   207  			break
   208  		}
   209  		o, err := pg.source.Next()
   210  		if err != nil {
   211  			return errors.Wrap(err, errSourceNext)
   212  		}
   213  		for _, pp := range pg.registry.unstructuredPreProcessors[o.Metadata.Category] {
   214  			if err := pp.PreProcess(o); err != nil {
   215  				return errors.Wrapf(err, errPreProcessFmt, o.Metadata.Category)
   216  			}
   217  		}
   218  	}
   219  	return nil
   220  }
   221  
   222  func (pg *PlanGenerator) convertPatchSets(o UnstructuredWithMetadata) ([]string, error) {
   223  	var converted []string
   224  	for _, psConv := range pg.registry.patchSetConverters {
   225  		if psConv.re == nil || psConv.converter == nil {
   226  			continue
   227  		}
   228  		if !psConv.re.MatchString(o.Object.GetName()) {
   229  			continue
   230  		}
   231  		c, err := ToComposition(o.Object)
   232  		if err != nil {
   233  			return nil, errors.Wrap(err, errUnstructuredConvert)
   234  		}
   235  		oldPatchSets := make([]xpv1.PatchSet, len(c.Spec.PatchSets))
   236  		for i, ps := range c.Spec.PatchSets {
   237  			oldPatchSets[i] = *ps.DeepCopy()
   238  		}
   239  		psMap := convertToMap(c.Spec.PatchSets)
   240  		if err := psConv.converter.PatchSets(psMap); err != nil {
   241  			return nil, errors.Wrapf(err, "failed to call PatchSet converter on Composition: %s", c.GetName())
   242  		}
   243  		newPatchSets := convertFromMap(psMap, oldPatchSets, true)
   244  		converted = append(converted, getConvertedPatchSetNames(newPatchSets, oldPatchSets)...)
   245  		pv := fieldpath.Pave(o.Object.Object)
   246  		if err := pv.SetValue("spec.patchSets", newPatchSets); err != nil {
   247  			return nil, errors.Wrapf(err, "failed to set converted patch sets on Composition: %s", c.GetName())
   248  		}
   249  	}
   250  	return converted, nil
   251  }
   252  
   253  func (pg *PlanGenerator) categoricalConvert(u *UnstructuredWithMetadata) error {
   254  	if u.Metadata.Category == categoryUnknown {
   255  		return nil
   256  	}
   257  	source := *u
   258  	source.Object = *u.Object.DeepCopy()
   259  	converters := pg.registry.categoricalConverters[u.Metadata.Category]
   260  	if converters == nil {
   261  		return nil
   262  	}
   263  	// TODO: if a categorical converter does not convert the given object,
   264  	// we will have a false positive. Better to compute and check
   265  	// a diff here.
   266  	for _, converter := range converters {
   267  		if err := converter.Convert(u); err != nil {
   268  			return errors.Wrapf(err, "failed to convert unstructured object of category: %s", u.Metadata.Category)
   269  		}
   270  	}
   271  	return pg.stepEditCategory(source, u)
   272  }
   273  
   274  func (pg *PlanGenerator) convert() error { //nolint: gocyclo
   275  	convertedMR := make(map[corev1.ObjectReference][]UnstructuredWithMetadata)
   276  	convertedComposition := make(map[string]string)
   277  	var composites []UnstructuredWithMetadata
   278  	var claims []UnstructuredWithMetadata
   279  	for hasNext, err := pg.source.HasNext(); ; hasNext, err = pg.source.HasNext() {
   280  		if err != nil {
   281  			return errors.Wrap(err, errSourceHasNext)
   282  		}
   283  		if !hasNext {
   284  			break
   285  		}
   286  		o, err := pg.source.Next()
   287  		if err != nil {
   288  			return errors.Wrap(err, errSourceNext)
   289  		}
   290  
   291  		if err := pg.categoricalConvert(&o); err != nil {
   292  			return err
   293  		}
   294  
   295  		switch gvk := o.Object.GroupVersionKind(); gvk {
   296  		case xppkgv1.ConfigurationGroupVersionKind:
   297  			if err := pg.convertConfigurationPackage(o); err != nil {
   298  				return errors.Wrapf(err, errConfigurationPackageMigrateFmt, o.Object.GetName())
   299  			}
   300  		case xpmetav1.ConfigurationGroupVersionKind, xpmetav1alpha1.ConfigurationGroupVersionKind:
   301  			if err := pg.convertConfigurationMetadata(o); err != nil {
   302  				return errors.Wrapf(err, errConfigurationMetadataMigrateFmt, o.Object.GetName())
   303  			}
   304  			pg.stepBackupAllResources()
   305  			pg.stepBuildConfiguration()
   306  			pg.stepPushConfiguration()
   307  		case xpv1.CompositionGroupVersionKind:
   308  			target, converted, err := pg.convertComposition(o)
   309  			if err != nil {
   310  				return errors.Wrapf(err, errCompositionMigrateFmt, o.Object.GetName())
   311  			}
   312  			if converted {
   313  				migratedName := fmt.Sprintf("%s-migrated", o.Object.GetName())
   314  				convertedComposition[o.Object.GetName()] = migratedName
   315  				target.Object.SetName(migratedName)
   316  				if err := pg.stepNewComposition(target); err != nil {
   317  					return errors.Wrapf(err, errCompositionMigrateFmt, o.Object.GetName())
   318  				}
   319  			}
   320  		case xppkgv1.ProviderGroupVersionKind:
   321  			isConverted, err := pg.convertProviderPackage(o)
   322  			if err != nil {
   323  				return errors.Wrap(err, errProviderMigrateFmt)
   324  			}
   325  			if isConverted {
   326  				if err := pg.stepDeleteMonolith(o); err != nil {
   327  					return err
   328  				}
   329  			}
   330  		case xppkgv1beta1.LockGroupVersionKind:
   331  			if err := pg.convertPackageLock(o); err != nil {
   332  				return errors.Wrapf(err, errLockMigrateFmt, o.Object.GetName())
   333  			}
   334  		default:
   335  			if o.Metadata.Category == CategoryComposite {
   336  				if err := pg.stepPauseComposite(&o); err != nil {
   337  					return errors.Wrap(err, errCompositePause)
   338  				}
   339  				composites = append(composites, o)
   340  				continue
   341  			}
   342  
   343  			if o.Metadata.Category == CategoryClaim {
   344  				claims = append(claims, o)
   345  				continue
   346  			}
   347  
   348  			targets, converted, err := pg.convertResource(o, false)
   349  			if err != nil {
   350  				return errors.Wrap(err, errResourceMigrate)
   351  			}
   352  			if converted {
   353  				convertedMR[corev1.ObjectReference{
   354  					Kind:       gvk.Kind,
   355  					Name:       o.Object.GetName(),
   356  					APIVersion: gvk.GroupVersion().String(),
   357  				}] = targets
   358  				for _, tu := range targets {
   359  					tu := tu
   360  					if err := pg.stepNewManagedResource(&tu); err != nil {
   361  						return errors.Wrap(err, errResourceMigrate)
   362  					}
   363  					if err := pg.stepStartManagedResource(&tu); err != nil {
   364  						return errors.Wrap(err, errResourceMigrate)
   365  					}
   366  				}
   367  			} else if _, ok, _ := toManagedResource(pg.registry.scheme, o.Object); ok {
   368  				if err := pg.stepStartManagedResource(&o); err != nil {
   369  					return errors.Wrap(err, errResourceMigrate)
   370  				}
   371  			}
   372  		}
   373  		if err := pg.addStepsForManagedResource(&o); err != nil {
   374  			return err
   375  		}
   376  	}
   377  	if err := pg.stepEditComposites(composites, convertedMR, convertedComposition); err != nil {
   378  		return errors.Wrap(err, errCompositesEdit)
   379  	}
   380  	if err := pg.stepStartComposites(composites); err != nil {
   381  		return errors.Wrap(err, errCompositesStart)
   382  	}
   383  	if err := pg.stepEditClaims(claims, convertedComposition); err != nil {
   384  		return errors.Wrap(err, errClaimsEdit)
   385  	}
   386  	return nil
   387  }
   388  
   389  func (pg *PlanGenerator) convertResource(o UnstructuredWithMetadata, compositionContext bool) ([]UnstructuredWithMetadata, bool, error) {
   390  	gvk := o.Object.GroupVersionKind()
   391  	conv := pg.registry.resourceConverters[gvk]
   392  	if conv == nil {
   393  		return []UnstructuredWithMetadata{o}, false, nil
   394  	}
   395  	// we have already ensured that the GVK belongs to a managed resource type
   396  	mg, _, err := toManagedResource(pg.registry.scheme, o.Object)
   397  	if err != nil {
   398  		return nil, false, errors.Wrap(err, errResourceMigrate)
   399  	}
   400  	if pg.registry.resourcePreProcessors != nil {
   401  		for _, pp := range pg.registry.resourcePreProcessors {
   402  			if err = pp.ResourcePreProcessor(mg); err != nil {
   403  				return nil, false, errors.Wrap(err, errResourceMigrate)
   404  			}
   405  		}
   406  	}
   407  	resources, err := conv.Resource(mg)
   408  	if err != nil {
   409  		return nil, false, errors.Wrap(err, errResourceMigrate)
   410  	}
   411  	if err := assertGVK(resources); err != nil {
   412  		return nil, true, errors.Wrap(err, errResourceMigrate)
   413  	}
   414  	if !compositionContext {
   415  		assertMetadataName(mg.GetName(), resources)
   416  	}
   417  	converted := make([]UnstructuredWithMetadata, 0, len(resources))
   418  	for _, mg := range resources {
   419  		converted = append(converted, UnstructuredWithMetadata{
   420  			Object:   ToSanitizedUnstructured(mg),
   421  			Metadata: o.Metadata,
   422  		})
   423  	}
   424  	return converted, true, nil
   425  }
   426  
   427  func assertGVK(resources []resource.Managed) error {
   428  	for _, r := range resources {
   429  		if reflect.ValueOf(r.GetObjectKind().GroupVersionKind()).IsZero() {
   430  			return errors.New(errMissingGVK)
   431  		}
   432  	}
   433  	return nil
   434  }
   435  
   436  func assertMetadataName(parentName string, resources []resource.Managed) {
   437  	for i, r := range resources {
   438  		if len(r.GetName()) != 0 || len(r.GetGenerateName()) != 0 {
   439  			continue
   440  		}
   441  		resources[i].SetGenerateName(fmt.Sprintf("%s-", parentName))
   442  	}
   443  }
   444  
   445  func (pg *PlanGenerator) convertComposition(o UnstructuredWithMetadata) (*UnstructuredWithMetadata, bool, error) { //nolint:gocyclo
   446  	convertedPS, err := pg.convertPatchSets(o)
   447  	if err != nil {
   448  		return nil, false, errors.Wrap(err, "failed to convert patch sets")
   449  	}
   450  	comp, err := ToComposition(o.Object)
   451  	if err != nil {
   452  		return nil, false, errors.Wrap(err, errUnstructuredConvert)
   453  	}
   454  	var targetResources []*xpv1.ComposedTemplate
   455  	isConverted := false
   456  	for _, cmp := range comp.Spec.Resources {
   457  		u, err := FromRawExtension(cmp.Base)
   458  		if err != nil {
   459  			return nil, false, errors.Wrapf(err, errCompositionMigrateFmt, o.Object.GetName())
   460  		}
   461  		gvk := u.GroupVersionKind()
   462  		converted, ok, err := pg.convertResource(UnstructuredWithMetadata{
   463  			Object:   u,
   464  			Metadata: o.Metadata,
   465  		}, true)
   466  		if err != nil {
   467  			return nil, false, errors.Wrap(err, errComposedTemplateBase)
   468  		}
   469  		isConverted = isConverted || ok
   470  		cmps := make([]*xpv1.ComposedTemplate, 0, len(converted))
   471  		sourceNameUsed := false
   472  		for _, u := range converted {
   473  			buff, err := u.Object.MarshalJSON()
   474  			if err != nil {
   475  				return nil, false, errors.Wrap(err, errUnstructuredMarshal)
   476  			}
   477  			c := cmp.DeepCopy()
   478  			c.Base = runtime.RawExtension{
   479  				Raw: buff,
   480  			}
   481  			if err := pg.setDefaultsOnTargetTemplate(cmp.Name, &sourceNameUsed, gvk, u.Object.GroupVersionKind(), c, comp.Spec.PatchSets, convertedPS); err != nil {
   482  				return nil, false, errors.Wrap(err, errComposedTemplateMigrate)
   483  			}
   484  			cmps = append(cmps, c)
   485  		}
   486  		conv := pg.registry.templateConverters[gvk]
   487  		if conv != nil {
   488  			if err := conv.ComposedTemplate(cmp, cmps...); err != nil {
   489  				return nil, false, errors.Wrap(err, errComposedTemplateMigrate)
   490  			}
   491  		}
   492  		targetResources = append(targetResources, cmps...)
   493  	}
   494  	comp.Spec.Resources = make([]xpv1.ComposedTemplate, 0, len(targetResources))
   495  	for _, cmp := range targetResources {
   496  		comp.Spec.Resources = append(comp.Spec.Resources, *cmp)
   497  	}
   498  	return &UnstructuredWithMetadata{
   499  		Object:   ToSanitizedUnstructured(&comp),
   500  		Metadata: o.Metadata,
   501  	}, isConverted, nil
   502  }
   503  
   504  func (pg *PlanGenerator) isGVKSkipped(sourceGVK schema.GroupVersionKind) bool {
   505  	for _, gvk := range pg.SkipGVKs {
   506  		if (len(gvk.Group) == 0 || gvk.Group == sourceGVK.Group) &&
   507  			(len(gvk.Version) == 0 || gvk.Version == sourceGVK.Version) &&
   508  			(len(gvk.Kind) == 0 || gvk.Kind == sourceGVK.Kind) {
   509  			return true
   510  		}
   511  	}
   512  	return false
   513  }
   514  
   515  func (pg *PlanGenerator) setDefaultsOnTargetTemplate(sourceName *string, sourceNameUsed *bool, gvkSource, gvkTarget schema.GroupVersionKind, target *xpv1.ComposedTemplate, patchSets []xpv1.PatchSet, convertedPS []string) error {
   516  	if pg.isGVKSkipped(gvkSource) {
   517  		return nil
   518  	}
   519  	// remove invalid patches that do not conform to the migration target's schema
   520  	if err := pg.removeInvalidPatches(gvkSource, gvkTarget, patchSets, target, convertedPS); err != nil {
   521  		return errors.Wrap(err, "failed to set the defaults on the migration target composed template")
   522  	}
   523  	if *sourceNameUsed || gvkSource.Kind != gvkTarget.Kind {
   524  		if sourceName != nil && len(*sourceName) > 0 {
   525  			targetName := fmt.Sprintf("%s-%s", *sourceName, rand.String(5))
   526  			target.Name = &targetName
   527  		}
   528  	} else {
   529  		*sourceNameUsed = true
   530  	}
   531  	return nil
   532  }
   533  
   534  func init() {
   535  	rand.Seed(time.Now().UnixNano())
   536  }