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 }