Skip to content

Commit

Permalink
Add support for converting Crossplane Configurations
Browse files Browse the repository at this point in the history
- Add the migration.Executor interface

Signed-off-by: Alper Rifat Ulucinar <[email protected]>
  • Loading branch information
ulucinar committed May 24, 2023
1 parent 7eb5f5a commit e506364
Show file tree
Hide file tree
Showing 8 changed files with 362 additions and 31 deletions.
49 changes: 49 additions & 0 deletions pkg/migration/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@ 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"
xpmetav1alpha1 "github.com/crossplane/crossplane/apis/pkg/meta/v1alpha1"
"github.com/pkg/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
Expand All @@ -27,6 +31,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"

Expand Down Expand Up @@ -165,3 +170,47 @@ 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 toConfigurationV1(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
}

func toConfigurationV1Alpha1(u unstructured.Unstructured) (*xpmetav1alpha1.Configuration, error) {
conf := &xpmetav1alpha1.Configuration{}
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, conf); err != nil {
return nil, errors.Wrap(err, errFromUnstructuredConf)
}
return conf, nil
}

func toConfiguration(u unstructured.Unstructured) (metav1.Object, error) {
var conf metav1.Object
var err error
switch u.GroupVersionKind().Version {
case "v1alpha1":
conf, err = toConfigurationV1Alpha1(u)
default:
conf, err = toConfigurationV1(u)
}
return conf, err
}
31 changes: 31 additions & 0 deletions pkg/migration/errors.go
Original file line number Diff line number Diff line change
@@ -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,
}
}
29 changes: 29 additions & 0 deletions pkg/migration/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ 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"
xpmetav1alpha1 "github.com/crossplane/crossplane/apis/pkg/meta/v1alpha1"
)

// ResourceConverter converts a managed resource from
Expand Down Expand Up @@ -69,6 +71,18 @@ type PatchSetConverter interface {
PatchSets(psMap map[string]*xpv1.PatchSet) error
}

// ConfigurationConverter converts a Crossplane Configuration's metadata.
type ConfigurationConverter interface {
// ConfigurationV1 takes a Crossplane Configuration v1 metadata,
// converts it, and stores the converted metadata in its argument.
// Returns any errors encountered during the conversion.
ConfigurationV1(configuration *xpmetav1.Configuration) error
// ConfigurationV1Alpha1 takes a Crossplane Configuration v1alpha1
// metadata, converts it, and stores the converted metadata in its
// argument. Returns any errors encountered during the conversion.
ConfigurationV1Alpha1(configuration *xpmetav1alpha1.Configuration) error
}

// Source is a source for reading resource manifests
type Source interface {
// HasNext returns `true` if the Source implementation has a next manifest
Expand All @@ -88,3 +102,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
}
66 changes: 49 additions & 17 deletions pkg/migration/plan_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,18 @@ import (
"strings"
"time"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

xpmetav1alpha1 "github.com/crossplane/crossplane/apis/pkg/meta/v1alpha1"

v1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
"github.com/crossplane/crossplane-runtime/pkg/meta"
"github.com/crossplane/crossplane-runtime/pkg/resource"
"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"
Expand All @@ -45,7 +50,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"
Expand Down Expand Up @@ -197,17 +203,25 @@ func (pg *PlanGenerator) convert() error { //nolint: gocyclo
return errors.Wrap(err, errSourceNext)
}
switch gvk := o.Object.GroupVersionKind(); gvk {
case xpmetav1.ConfigurationGroupVersionKind, xpmetav1alpha1.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:
Expand Down Expand Up @@ -314,20 +328,38 @@ 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)
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)
func (pg *PlanGenerator) convertConfiguration(o UnstructuredWithMetadata) (*UnstructuredWithMetadata, bool, error) {
isConverted := false
var conf metav1.Object
var err error
for _, confConv := range pg.registry.configurationConverters {
if confConv.re == nil || confConv.converter == nil || !confConv.re.MatchString(o.Object.GetName()) {
continue
}

conf, err = toConfiguration(o.Object)
if err != nil {
return nil, false, err
}
switch o.Object.GroupVersionKind().Version {
case "v1alpha1":
err = confConv.converter.ConfigurationV1Alpha1(conf.(*xpmetav1alpha1.Configuration))
default:
err = confConv.converter.ConfigurationV1(conf.(*xpmetav1.Configuration))
}
if err != nil {
return nil, false, errors.Wrapf(err, "failed to call converter on Configuration: %s", conf.GetName())
}
// TODO: if a configuration converter only converts a specific version,
// (or does not convert the given configuration),
// we will have a false positive. Better to compute and check
// a diff here.
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
Expand All @@ -344,7 +376,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{
Expand Down
58 changes: 56 additions & 2 deletions pkg/migration/plan_generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@ import (
"path/filepath"
"testing"

xpmetav1alpha1 "github.com/crossplane/crossplane/apis/pkg/meta/v1alpha1"

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"
Expand Down Expand Up @@ -95,7 +98,7 @@ func TestGeneratePlan(t *testing.T) {
re: AllCompositions,
converter: &testConverter{},
},
}),
}, nil),
},
want: want{
migrationPlanPath: "testdata/plan/generated/migration_plan.yaml",
Expand All @@ -112,6 +115,34 @@ func TestGeneratePlan(t *testing.T) {
},
},
},
"PlanWithConfigurationV1": {
fields: fields{
source: newTestSource(map[string]Metadata{
"testdata/plan/configurationv1.yaml": {}}),
target: newTestTarget(),
registry: getRegistryWithConverters(nil, nil, []configurationConverter{
{
re: AllConfigurations,
converter: &testConverter{},
},
}),
},
want: want{},
},
"PlanWithConfigurationV1Alpha1": {
fields: fields{
source: newTestSource(map[string]Metadata{
"testdata/plan/configurationv1alpha1.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) {
Expand Down Expand Up @@ -239,6 +270,26 @@ func (f *testTarget) Delete(o UnstructuredWithMetadata) error {

type testConverter struct{}

func (f *testConverter) ConfigurationV1(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) ConfigurationV1Alpha1(c *xpmetav1alpha1.Configuration) error {
c.Spec.DependsOn = []xpmetav1alpha1.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"]`)
Expand All @@ -249,14 +300,17 @@ 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{})
r := NewRegistry(scheme)
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)
}
Expand Down
Loading

0 comments on commit e506364

Please sign in to comment.