diff --git a/pkg/migration/converter.go b/pkg/migration/converter.go index e2937c96..52a0761d 100644 --- a/pkg/migration/converter.go +++ b/pkg/migration/converter.go @@ -16,7 +16,9 @@ package migration import ( "github.com/crossplane/crossplane-runtime/pkg/fieldpath" + "github.com/crossplane/crossplane-runtime/pkg/resource" xpv1 "github.com/crossplane/crossplane/apis/apiextensions/v1" + xpmetav1 "github.com/crossplane/crossplane/apis/pkg/meta/v1" "github.com/pkg/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -27,6 +29,7 @@ import ( const ( errFromUnstructured = "failed to convert from unstructured.Unstructured to the managed resource type" + errFromUnstructuredConf = "failed to convert from unstructured.Unstructured to Crossplane Configuration metadata" errToUnstructured = "failed to convert from the managed resource type to unstructured.Unstructured" errRawExtensionUnmarshal = "failed to unmarshal runtime.RawExtension" @@ -165,3 +168,27 @@ func addNameGVK(u unstructured.Unstructured, target map[string]any) map[string]a target["metadata"] = m return target } + +func toManagedResource(c runtime.ObjectCreater, u unstructured.Unstructured) (resource.Managed, bool, error) { + gvk := u.GroupVersionKind() + if gvk == xpv1.CompositionGroupVersionKind { + return nil, false, nil + } + obj, err := c.New(gvk) + if err != nil { + return nil, false, errors.Wrapf(err, errFmtNewObject, gvk) + } + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, obj); err != nil { + return nil, false, errors.Wrap(err, errFromUnstructured) + } + mg, ok := obj.(resource.Managed) + return mg, ok, nil +} + +func toConfiguration(u unstructured.Unstructured) (*xpmetav1.Configuration, error) { + conf := &xpmetav1.Configuration{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, conf); err != nil { + return nil, errors.Wrap(err, errFromUnstructuredConf) + } + return conf, nil +} diff --git a/pkg/migration/errors.go b/pkg/migration/errors.go new file mode 100644 index 00000000..b28b7b9c --- /dev/null +++ b/pkg/migration/errors.go @@ -0,0 +1,31 @@ +// Copyright 2023 Upbound Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package migration + +import "fmt" + +type errUnsupportedStepType struct { + planStep Step +} + +func (e errUnsupportedStepType) Error() string { + return fmt.Sprintf("executor does not support steps of type %q in step: %s", e.planStep.Type, e.planStep.Name) +} + +func NewUnsupportedStepTypeError(s Step) error { + return errUnsupportedStepType{ + planStep: s, + } +} diff --git a/pkg/migration/interfaces.go b/pkg/migration/interfaces.go index 12bc1f86..144134bf 100644 --- a/pkg/migration/interfaces.go +++ b/pkg/migration/interfaces.go @@ -17,6 +17,7 @@ package migration import ( "github.com/crossplane/crossplane-runtime/pkg/resource" xpv1 "github.com/crossplane/crossplane/apis/apiextensions/v1" + xpmetav1 "github.com/crossplane/crossplane/apis/pkg/meta/v1" ) // ResourceConverter converts a managed resource from @@ -69,6 +70,14 @@ type PatchSetConverter interface { PatchSets(psMap map[string]*xpv1.PatchSet) error } +// ConfigurationConverter converts a Crossplane Configuration's metadata. +type ConfigurationConverter interface { + // Configuration takes a Crossplane Configuration metadata, converts it, + // and stores the converted metadata in its argument. Returns any errors + // encountered during the conversion. + Configuration(configuration *xpmetav1.Configuration) error +} + // Source is a source for reading resource manifests type Source interface { // HasNext returns `true` if the Source implementation has a next manifest @@ -88,3 +97,18 @@ type Target interface { // Delete deletes a resource manifest from this Target Delete(o UnstructuredWithMetadata) error } + +// Executor is a migration plan executor. +type Executor interface { + // Init initializes an executor using the supplied executor specific + // configuration data. + Init(config any) error + // Step asks the executor to execute the next step passing any available + // context from the previous step, and returns any new context to be passed + // to the next step if there exists one. + Step(s Step, ctx any) (any, error) + // Destroy is called when all the steps have been executed, + // or a step has returned an error, and we would like to stop + // executing the plan. + Destroy() error +} diff --git a/pkg/migration/plan_generator.go b/pkg/migration/plan_generator.go index 05996041..345fa830 100644 --- a/pkg/migration/plan_generator.go +++ b/pkg/migration/plan_generator.go @@ -28,6 +28,7 @@ import ( "github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/claim" "github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/composite" xpv1 "github.com/crossplane/crossplane/apis/apiextensions/v1" + xpmetav1 "github.com/crossplane/crossplane/apis/pkg/meta/v1" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -45,7 +46,8 @@ const ( errCompositePause = "failed to pause composite resource" errCompositesEdit = "failed to edit composite resources" errCompositesStart = "failed to start composite resources" - errCompositionMigrate = "failed to migrate the composition" + errCompositionMigrateFmt = "failed to migrate the composition: %s" + errConfigurationMigrateFmt = "failed to migrate the configuration: %s" errComposedTemplateBase = "failed to migrate the base of a composed template" errComposedTemplateMigrate = "failed to migrate the composed templates of the composition" errResourceOutput = "failed to output migrated resource" @@ -197,17 +199,25 @@ func (pg *PlanGenerator) convert() error { //nolint: gocyclo return errors.Wrap(err, errSourceNext) } switch gvk := o.Object.GroupVersionKind(); gvk { + case xpmetav1.ConfigurationGroupVersionKind: + target, converted, err := pg.convertConfiguration(o) + if err != nil { + return errors.Wrapf(err, errConfigurationMigrateFmt, o.Object.GetName()) + } + if converted { + fmt.Printf("converted configuration: %v\n", target) + } case xpv1.CompositionGroupVersionKind: target, converted, err := pg.convertComposition(o) if err != nil { - return errors.Wrap(err, errCompositionMigrate) + return errors.Wrapf(err, errCompositionMigrateFmt, o.Object.GetName()) } if converted { migratedName := fmt.Sprintf("%s-migrated", o.Object.GetName()) convertedComposition[o.Object.GetName()] = migratedName target.Object.SetName(migratedName) if err := pg.stepNewComposition(target); err != nil { - return errors.Wrap(err, errCompositionMigrate) + return errors.Wrapf(err, errCompositionMigrateFmt, o.Object.GetName()) } } default: @@ -314,20 +324,25 @@ func assertMetadataName(parentName string, resources []resource.Managed) { } } -func toManagedResource(c runtime.ObjectCreater, u unstructured.Unstructured) (resource.Managed, bool, error) { - gvk := u.GroupVersionKind() - if gvk == xpv1.CompositionGroupVersionKind { - return nil, false, nil - } - obj, err := c.New(gvk) +func (pg *PlanGenerator) convertConfiguration(o UnstructuredWithMetadata) (*UnstructuredWithMetadata, bool, error) { + conf, err := toConfiguration(o.Object) if err != nil { - return nil, false, errors.Wrapf(err, errFmtNewObject, gvk) + return nil, false, err } - if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, obj); err != nil { - return nil, false, errors.Wrap(err, errFromUnstructured) + isConverted := false + for _, confConv := range pg.registry.configurationConverters { + if confConv.re == nil || confConv.converter == nil || !confConv.re.MatchString(o.Object.GetName()) { + continue + } + if err := confConv.converter.Configuration(conf); err != nil { + return nil, false, errors.Wrapf(err, "failed to call converter on Configuration: %s", conf.GetName()) + } + isConverted = true } - mg, ok := obj.(resource.Managed) - return mg, ok, nil + return &UnstructuredWithMetadata{ + Object: ToSanitizedUnstructured(conf), + Metadata: o.Metadata, + }, isConverted, nil } func (pg *PlanGenerator) convertComposition(o UnstructuredWithMetadata) (*UnstructuredWithMetadata, bool, error) { // nolint:gocyclo @@ -344,7 +359,7 @@ func (pg *PlanGenerator) convertComposition(o UnstructuredWithMetadata) (*Unstru for _, cmp := range comp.Spec.Resources { u, err := FromRawExtension(cmp.Base) if err != nil { - return nil, false, errors.Wrap(err, errCompositionMigrate) + return nil, false, errors.Wrapf(err, errCompositionMigrateFmt, o.Object.GetName()) } gvk := u.GroupVersionKind() converted, ok, err := pg.convertResource(UnstructuredWithMetadata{ diff --git a/pkg/migration/plan_generator_test.go b/pkg/migration/plan_generator_test.go index d240db27..fcf2629e 100644 --- a/pkg/migration/plan_generator_test.go +++ b/pkg/migration/plan_generator_test.go @@ -23,6 +23,7 @@ import ( xpresource "github.com/crossplane/crossplane-runtime/pkg/resource" "github.com/crossplane/crossplane-runtime/pkg/test" v1 "github.com/crossplane/crossplane/apis/apiextensions/v1" + xpmetav1 "github.com/crossplane/crossplane/apis/pkg/meta/v1" "github.com/google/go-cmp/cmp" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -95,7 +96,7 @@ func TestGeneratePlan(t *testing.T) { re: AllCompositions, converter: &testConverter{}, }, - }), + }, nil), }, want: want{ migrationPlanPath: "testdata/plan/generated/migration_plan.yaml", @@ -112,6 +113,20 @@ func TestGeneratePlan(t *testing.T) { }, }, }, + "PlanWithConfiguration": { + fields: fields{ + source: newTestSource(map[string]Metadata{ + "testdata/plan/configuration.yaml": {}}), + target: newTestTarget(), + registry: getRegistryWithConverters(nil, nil, []configurationConverter{ + { + re: AllConfigurations, + converter: &testConverter{}, + }, + }), + }, + want: want{}, + }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { @@ -239,6 +254,16 @@ func (f *testTarget) Delete(o UnstructuredWithMetadata) error { type testConverter struct{} +func (f *testConverter) Configuration(c *xpmetav1.Configuration) error { + c.Spec.DependsOn = []xpmetav1.Dependency{ + { + Provider: ptrFromString("xpkg.upbound.io/upbound/provider-aws-eks"), + Version: ">=v0.17.0", + }, + } + return nil +} + func (f *testConverter) PatchSets(psMap map[string]*v1.PatchSet) error { psMap["ps1"].Patches[0].ToFieldPath = ptrFromString(`spec.forProvider.tags["key3"]`) psMap["ps6"].Patches[0].ToFieldPath = ptrFromString(`spec.forProvider.tags["key4"]`) @@ -249,7 +274,7 @@ func ptrFromString(s string) *string { return &s } -func getRegistryWithConverters(converters map[schema.GroupVersionKind]delegatingConverter, psConverters []patchSetConverter) *Registry { +func getRegistryWithConverters(converters map[schema.GroupVersionKind]delegatingConverter, psConverters []patchSetConverter, confConverters []configurationConverter) *Registry { scheme := runtime.NewScheme() scheme.AddKnownTypeWithName(fake.MigrationSourceGVK, &fake.MigrationSourceObject{}) scheme.AddKnownTypeWithName(fake.MigrationTargetGVK, &fake.MigrationTargetObject{}) @@ -257,6 +282,9 @@ func getRegistryWithConverters(converters map[schema.GroupVersionKind]delegating for _, c := range psConverters { r.RegisterPatchSetConverter(c.re, c.converter) } + for _, c := range confConverters { + r.RegisterConfigurationConverter(c.re, c.converter) + } for gvk, d := range converters { r.RegisterConversionFunctions(gvk, d.rFn, d.cmpFn, nil) } diff --git a/pkg/migration/registry.go b/pkg/migration/registry.go index a1f022b2..0408fe26 100644 --- a/pkg/migration/registry.go +++ b/pkg/migration/registry.go @@ -19,6 +19,7 @@ import ( "github.com/crossplane/crossplane-runtime/pkg/resource" xpv1 "github.com/crossplane/crossplane/apis/apiextensions/v1" + xpmetav1 "github.com/crossplane/crossplane/apis/pkg/meta/v1" "github.com/pkg/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -27,6 +28,8 @@ import ( var ( // AllCompositions matches all v1.Composition names. AllCompositions = regexp.MustCompile(`.*`) + // AllConfigurations matches all metav1.Configuration names. + AllConfigurations = regexp.MustCompile(`.*`) ) const ( @@ -45,16 +48,27 @@ type patchSetConverter struct { converter PatchSetConverter } +type configurationConverter struct { + // re is the regular expression against which a Configuration's name + // will be matched to determine whether the conversion function + // will be invoked. + re *regexp.Regexp + // converter is the ConfigurationConverter to be run on the Configuration's + // metadata. + converter ConfigurationConverter +} + // Registry is a registry of `migration.Converter`s keyed with // the associated `schema.GroupVersionKind`s and an associated // runtime.Scheme with which the corresponding types are registered. type Registry struct { - resourceConverters map[schema.GroupVersionKind]ResourceConverter - templateConverters map[schema.GroupVersionKind]ComposedTemplateConverter - patchSetConverters []patchSetConverter - scheme *runtime.Scheme - claimTypes []schema.GroupVersionKind - compositeTypes []schema.GroupVersionKind + resourceConverters map[schema.GroupVersionKind]ResourceConverter + templateConverters map[schema.GroupVersionKind]ComposedTemplateConverter + patchSetConverters []patchSetConverter + configurationConverters []configurationConverter + scheme *runtime.Scheme + claimTypes []schema.GroupVersionKind + compositeTypes []schema.GroupVersionKind } // NewRegistry returns a new Registry initialized with @@ -102,7 +116,7 @@ func (r *Registry) RegisterCompositionConverter(gvk schema.GroupVersionKind, con r.RegisterTemplateConverter(gvk, conv) } -// RegisterPatchSetConverter registers the given PatchSetConversionFn for +// RegisterPatchSetConverter registers the given PatchSetConverter for // the compositions whose name match the given regular expression. func (r *Registry) RegisterPatchSetConverter(re *regexp.Regexp, psConv PatchSetConverter) { r.patchSetConverters = append(r.patchSetConverters, patchSetConverter{ @@ -111,6 +125,21 @@ func (r *Registry) RegisterPatchSetConverter(re *regexp.Regexp, psConv PatchSetC }) } +// RegisterConfigurationConverter registers the given ConfigurationConverter +// for the configurations whose name match the given regular expression. +func (r *Registry) RegisterConfigurationConverter(re *regexp.Regexp, confConv ConfigurationConverter) { + r.configurationConverters = append(r.configurationConverters, configurationConverter{ + re: re, + converter: confConv, + }) +} + +func (r *Registry) RegisterConfigurationConversionFunction(re *regexp.Regexp, confConversionFn ConfigurationConversionFn) { + r.RegisterConfigurationConverter(re, &delegatingConverter{ + confFn: confConversionFn, + }) +} + // AddToScheme registers types with this Registry's runtime.Scheme func (r *Registry) AddToScheme(sb func(scheme *runtime.Scheme) error) error { return errors.Wrap(sb(r.scheme), errAddToScheme) @@ -156,13 +185,14 @@ func (r *Registry) GetCompositionGVKs() []schema.GroupVersionKind { } // GetAllRegisteredGVKs returns a list of registered GVKs -// including v1.CompositionGroupVersionKind +// including v1.CompositionGroupVersionKind and +// metav1.ConfigurationGroupVersionKind. func (r *Registry) GetAllRegisteredGVKs() []schema.GroupVersionKind { gvks := make([]schema.GroupVersionKind, 0, len(r.claimTypes)+len(r.compositeTypes)+len(r.resourceConverters)+len(r.templateConverters)+1) gvks = append(gvks, r.claimTypes...) gvks = append(gvks, r.compositeTypes...) gvks = append(gvks, r.GetManagedResourceGVKs()...) - gvks = append(gvks, xpv1.CompositionGroupVersionKind) + gvks = append(gvks, xpv1.CompositionGroupVersionKind, xpmetav1.ConfigurationGroupVersionKind) return gvks } @@ -180,10 +210,23 @@ type ComposedTemplateConversionFn func(sourceTemplate xpv1.ComposedTemplate, con // schema to the migration target provider's schema. type PatchSetsConversionFn func(psMap map[string]*xpv1.PatchSet) error +// ConfigurationConversionFn is a function that converts the specified +// migration source Configuration metadata to the migration target +// Configuration metadata. +type ConfigurationConversionFn func(configuration *xpmetav1.Configuration) error + type delegatingConverter struct { - rFn ResourceConversionFn - cmpFn ComposedTemplateConversionFn - psFn PatchSetsConversionFn + rFn ResourceConversionFn + cmpFn ComposedTemplateConversionFn + psFn PatchSetsConversionFn + confFn ConfigurationConversionFn +} + +func (d *delegatingConverter) Configuration(c *xpmetav1.Configuration) error { + if d.confFn == nil { + return nil + } + return d.confFn(c) } func (d *delegatingConverter) PatchSets(psMap map[string]*xpv1.PatchSet) error { diff --git a/pkg/migration/testdata/plan/configuration.yaml b/pkg/migration/testdata/plan/configuration.yaml new file mode 100644 index 00000000..809b0a26 --- /dev/null +++ b/pkg/migration/testdata/plan/configuration.yaml @@ -0,0 +1,36 @@ +apiVersion: meta.pkg.crossplane.io/v1 +kind: Configuration +metadata: + name: platform-ref-aws + annotations: + meta.crossplane.io/maintainer: Upbound + meta.crossplane.io/source: github.com/upbound/platform-ref-aws + meta.crossplane.io/license: Apache-2.0 + meta.crossplane.io/description: | + This reference platform Configuration for Kubernetes and Data Services + is a starting point to build, run, and operate your own internal cloud + platform and offer a self-service console and API to your internal teams. + + meta.crossplane.io/readme: | + This reference platform `Configuration` for Kubernetes and Data Services + is a starting point to build, run, and operate your own internal cloud + platform and offer a self-service console and API to your internal teams. + It provides platform APIs to provision fully configured EKS clusters, + with secure networking, and stateful cloud services (RDS) designed to + securely connect to the nodes in each EKS cluster -- all composed using + cloud service primitives from the [Upbound Official AWS + Provider](https://marketplace.upbound.io/providers/upbound/provider-aws). App + deployments can securely connect to the infrastructure they need using + secrets distributed directly to the app namespace. + + To learn more checkout the [GitHub + repo](https://github.com/upbound/platform-ref-aws/) that you can copy and + customize to meet the exact needs of your organization! +spec: + crossplane: + version: ">=v1.7.0-0" + dependsOn: + - provider: xpkg.upbound.io/upbound/provider-aws + version: ">=v0.15.0" + - provider: xpkg.upbound.io/crossplane-contrib/provider-helm + version: ">=v0.12.0" \ No newline at end of file