diff --git a/pkg/migration/api_steps.go b/pkg/migration/api_steps.go new file mode 100644 index 00000000..f20a72e3 --- /dev/null +++ b/pkg/migration/api_steps.go @@ -0,0 +1,265 @@ +// 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" + "strconv" + + v1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/meta" + "github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/claim" + "github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/composite" + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +const ( + stepPauseManaged step = iota + stepPauseComposites + stepCreateNewManaged + stepNewCompositions + stepEditComposites + stepEditClaims + stepDeletionPolicyOrphan + stepDeleteOldManaged + stepStartManaged + stepStartComposites + // this must be the last step + stepAPIEnd +) + +func (pg *PlanGenerator) addStepsForManagedResource(u *UnstructuredWithMetadata) error { + if _, ok, err := toManagedResource(pg.registry.scheme, u.Object); err != nil || !ok { + // not a managed resource or unable to determine + // whether it's a managed resource + return nil // nolint:nilerr + } + qName := getQualifiedName(u.Object) + if err := pg.stepPauseManagedResource(u, qName); err != nil { + return err + } + if err := pg.stepOrphanManagedResource(u, qName); err != nil { + return err + } + pg.stepDeleteOldManagedResource(u) + return nil +} + +func (pg *PlanGenerator) stepStartManagedResource(u *UnstructuredWithMetadata) error { + u.Metadata.Path = fmt.Sprintf("%s/%s.yaml", pg.stepAPI(stepStartManaged).Name, getQualifiedName(u.Object)) + pg.stepAPI(stepStartManaged).Patch.Files = append(pg.stepAPI(stepStartManaged).Patch.Files, u.Metadata.Path) + return pg.pause(*u, false) +} + +func (pg *PlanGenerator) stepPauseManagedResource(u *UnstructuredWithMetadata, qName string) error { + u.Metadata.Path = fmt.Sprintf("%s/%s.yaml", pg.stepAPI(stepPauseManaged).Name, qName) + pg.stepAPI(stepPauseManaged).Patch.Files = append(pg.stepAPI(stepPauseManaged).Patch.Files, u.Metadata.Path) + return pg.pause(*u, true) +} + +func (pg *PlanGenerator) stepPauseComposite(u *UnstructuredWithMetadata) error { + u.Metadata.Path = fmt.Sprintf("%s/%s.yaml", pg.stepAPI(stepPauseComposites).Name, getQualifiedName(u.Object)) + pg.stepAPI(stepPauseComposites).Patch.Files = append(pg.stepAPI(stepPauseComposites).Patch.Files, u.Metadata.Path) + return pg.pause(*u, true) +} + +func (pg *PlanGenerator) stepOrphanManagedResource(u *UnstructuredWithMetadata, qName string) error { + u.Metadata.Path = fmt.Sprintf("%s/%s.yaml", pg.stepAPI(stepDeletionPolicyOrphan).Name, qName) + pg.stepAPI(stepDeletionPolicyOrphan).Patch.Files = append(pg.stepAPI(stepDeletionPolicyOrphan).Patch.Files, u.Metadata.Path) + return errors.Wrap(pg.target.Put(UnstructuredWithMetadata{ + Object: unstructured.Unstructured{ + Object: addNameGVK(u.Object, map[string]any{ + "spec": map[string]any{ + "deletionPolicy": string(v1.DeletionOrphan), + }, + }), + }, + Metadata: u.Metadata, + }), errResourceOrphan) +} + +func (pg *PlanGenerator) stepDeleteOldManagedResource(u *UnstructuredWithMetadata) { + pg.stepAPI(stepDeleteOldManaged).Delete.Resources = append(pg.stepAPI(stepDeleteOldManaged).Delete.Resources, + Resource{ + GroupVersionKind: FromGroupVersionKind(u.Object.GroupVersionKind()), + Name: u.Object.GetName(), + }) +} + +func (pg *PlanGenerator) stepNewManagedResource(u *UnstructuredWithMetadata) error { + meta.AddAnnotations(&u.Object, map[string]string{meta.AnnotationKeyReconciliationPaused: "true"}) + u.Metadata.Path = fmt.Sprintf("%s/%s.yaml", pg.stepAPI(stepCreateNewManaged).Name, getQualifiedName(u.Object)) + pg.stepAPI(stepCreateNewManaged).Apply.Files = append(pg.stepAPI(stepCreateNewManaged).Apply.Files, u.Metadata.Path) + if err := pg.target.Put(*u); err != nil { + return errors.Wrap(err, errResourceOutput) + } + return nil +} + +func (pg *PlanGenerator) stepNewComposition(u *UnstructuredWithMetadata) error { + u.Metadata.Path = fmt.Sprintf("%s/%s.yaml", pg.stepAPI(stepNewCompositions).Name, getQualifiedName(u.Object)) + pg.stepAPI(stepNewCompositions).Apply.Files = append(pg.stepAPI(stepNewCompositions).Apply.Files, u.Metadata.Path) + if err := pg.target.Put(*u); err != nil { + return errors.Wrap(err, errCompositionOutput) + } + return nil +} + +func (pg *PlanGenerator) stepStartComposites(composites []UnstructuredWithMetadata) error { + for _, u := range composites { + u.Metadata.Path = fmt.Sprintf("%s/%s.yaml", pg.stepAPI(stepStartComposites).Name, getQualifiedName(u.Object)) + pg.stepAPI(stepStartComposites).Patch.Files = append(pg.stepAPI(stepStartComposites).Patch.Files, u.Metadata.Path) + if err := pg.pause(u, false); err != nil { + return errors.Wrap(err, errCompositeOutput) + } + } + return nil +} + +func (pg *PlanGenerator) pause(u UnstructuredWithMetadata, isPaused bool) error { + return errors.Wrap(pg.target.Put(UnstructuredWithMetadata{ + Object: unstructured.Unstructured{ + Object: addNameGVK(u.Object, map[string]any{ + "metadata": map[string]any{ + "annotations": map[string]any{ + meta.AnnotationKeyReconciliationPaused: strconv.FormatBool(isPaused), + }, + }, + }), + }, + Metadata: Metadata{ + Path: u.Metadata.Path, + }, + }), errPause) +} + +func (pg *PlanGenerator) stepEditComposites(composites []UnstructuredWithMetadata, convertedMap map[corev1.ObjectReference][]UnstructuredWithMetadata, convertedComposition map[string]string) error { + for _, u := range composites { + cp := composite.Unstructured{Unstructured: u.Object} + refs := cp.GetResourceReferences() + // compute new spec.resourceRefs so that the XR references the new MRs + newRefs := make([]corev1.ObjectReference, 0, len(refs)) + for _, ref := range refs { + converted, ok := convertedMap[ref] + if !ok { + newRefs = append(newRefs, ref) + continue + } + for _, o := range converted { + gvk := o.Object.GroupVersionKind() + newRefs = append(newRefs, corev1.ObjectReference{ + Kind: gvk.Kind, + Name: o.Object.GetName(), + APIVersion: gvk.GroupVersion().String(), + }) + } + } + cp.SetResourceReferences(newRefs) + // compute new spec.compositionRef + if ref := cp.GetCompositionReference(); ref != nil && convertedComposition[ref.Name] != "" { + ref.Name = convertedComposition[ref.Name] + cp.SetCompositionReference(ref) + } + spec := u.Object.Object["spec"].(map[string]any) + u.Metadata.Path = fmt.Sprintf("%s/%s.yaml", pg.stepAPI(stepEditComposites).Name, getQualifiedName(u.Object)) + pg.stepAPI(stepEditComposites).Patch.Files = append(pg.stepAPI(stepEditComposites).Patch.Files, u.Metadata.Path) + if err := pg.target.Put(UnstructuredWithMetadata{ + Object: unstructured.Unstructured{ + Object: addNameGVK(u.Object, map[string]any{ + "spec": map[string]any{ + keyResourceRefs: spec[keyResourceRefs], + keyCompositionRef: spec[keyCompositionRef]}, + }), + }, + Metadata: u.Metadata, + }); err != nil { + return errors.Wrap(err, errCompositeOutput) + } + } + return nil +} + +func (pg *PlanGenerator) stepEditClaims(claims []UnstructuredWithMetadata, convertedComposition map[string]string) error { + for _, u := range claims { + cm := claim.Unstructured{Unstructured: u.Object} + if ref := cm.GetCompositionReference(); ref != nil && convertedComposition[ref.Name] != "" { + ref.Name = convertedComposition[ref.Name] + cm.SetCompositionReference(ref) + } + u.Metadata.Path = fmt.Sprintf("%s/%s.yaml", pg.stepAPI(stepEditClaims).Name, getQualifiedName(u.Object)) + pg.stepAPI(stepEditClaims).Patch.Files = append(pg.stepAPI(stepEditClaims).Patch.Files, u.Metadata.Path) + if err := pg.target.Put(UnstructuredWithMetadata{ + Object: unstructured.Unstructured{ + Object: addNameGVK(u.Object, map[string]any{ + "spec": map[string]any{ + keyCompositionRef: u.Object.Object["spec"].(map[string]any)[keyCompositionRef], + }, + }), + }, + Metadata: u.Metadata, + }); err != nil { + return errors.Wrap(err, errClaimOutput) + } + } + return nil +} + +// NOTE: to cover different migration scenarios, we may use +// "migration templates" instead of a static plan. But a static plan should be +// fine as a start. +func (pg *PlanGenerator) stepAPI(s step) *Step { // nolint:gocyclo // all steps under a single clause for readability + stepKey := strconv.Itoa(int(s)) + if pg.Plan.Spec.stepMap[stepKey] != nil { + return pg.Plan.Spec.stepMap[stepKey] + } + + pg.Plan.Spec.stepMap[stepKey] = &Step{} + switch s { // nolint:exhaustive + case stepPauseManaged: + setPatchStep("pause-managed", pg.Plan.Spec.stepMap[stepKey]) + + case stepPauseComposites: + setPatchStep("pause-composites", pg.Plan.Spec.stepMap[stepKey]) + + case stepCreateNewManaged: + setApplyStep("create-new-managed", pg.Plan.Spec.stepMap[stepKey]) + + case stepNewCompositions: + setApplyStep("new-compositions", pg.Plan.Spec.stepMap[stepKey]) + + case stepEditComposites: + setPatchStep("edit-composites", pg.Plan.Spec.stepMap[stepKey]) + + case stepEditClaims: + setPatchStep("edit-claims", pg.Plan.Spec.stepMap[stepKey]) + + case stepDeletionPolicyOrphan: + setPatchStep("deletion-policy-orphan", pg.Plan.Spec.stepMap[stepKey]) + + case stepDeleteOldManaged: + setDeleteStep("delete-old-managed", pg.Plan.Spec.stepMap[stepKey]) + + case stepStartManaged: + setPatchStep("start-managed", pg.Plan.Spec.stepMap[stepKey]) + + case stepStartComposites: + setPatchStep("start-composites", pg.Plan.Spec.stepMap[stepKey]) + default: + panic(fmt.Sprintf(errInvalidStepFmt, s)) + } + return pg.Plan.Spec.stepMap[stepKey] +} diff --git a/pkg/migration/configurationmetadata_steps.go b/pkg/migration/configurationmetadata_steps.go new file mode 100644 index 00000000..9a896871 --- /dev/null +++ b/pkg/migration/configurationmetadata_steps.go @@ -0,0 +1,143 @@ +// 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" + "strconv" + + xpmetav1 "github.com/crossplane/crossplane/apis/pkg/meta/v1" + xpmetav1alpha1 "github.com/crossplane/crossplane/apis/pkg/meta/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +const ( + // configuration migration steps follow any existing API migration steps + stepNewServiceScopedProvider = iota + stepAPIEnd + 1 + stepConfigurationPackageDisableDepResolution + stepEditPackageLock + stepDeleteMonolithicProvider + stepActivateServiceScopedProviderRevision + stepEditConfigurationMetadata + stepEditConfigurationPackage + stepConfigurationPackageEnableDepResolution +) + +const ( + errConfigurationMetadataOutput = "failed to output configuration JSON merge document" +) + +func (pg *PlanGenerator) convertConfigurationMetadata(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 = toConfigurationMetadata(o.Object) + if err != nil { + return nil, false, err + } + switch o.Object.GroupVersionKind().Version { + case "v1alpha1": + err = confConv.converter.ConfigurationMetadataV1Alpha1(conf.(*xpmetav1alpha1.Configuration)) + default: + err = confConv.converter.ConfigurationMetadataV1(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 + } + return &UnstructuredWithMetadata{ + Object: ToSanitizedUnstructured(conf), + Metadata: o.Metadata, + }, isConverted, nil +} + +func (pg *PlanGenerator) stepConfiguration(s step) *Step { + return pg.stepConfigurationWithSubStep(s, false) +} + +func (pg *PlanGenerator) configurationSubStep(s step) string { + ss := -1 + subStep := pg.subSteps[s] + if subStep != "" { + s, err := strconv.Atoi(subStep) + if err == nil { + ss = s + } + } + pg.subSteps[s] = strconv.Itoa(ss + 1) + return pg.subSteps[s] +} + +func (pg *PlanGenerator) stepConfigurationWithSubStep(s step, newSubStep bool) *Step { // nolint:gocyclo // easy to follow all steps here + stepKey := strconv.Itoa(int(s)) + if newSubStep { + stepKey = fmt.Sprintf("%s.%s", stepKey, pg.configurationSubStep(s)) + } + if pg.Plan.Spec.stepMap[stepKey] != nil { + return pg.Plan.Spec.stepMap[stepKey] + } + + pg.Plan.Spec.stepMap[stepKey] = &Step{} + switch s { // nolint:gocritic,exhaustive + case stepNewServiceScopedProvider: + setApplyStep("new-ssop", pg.Plan.Spec.stepMap[stepKey]) + case stepConfigurationPackageDisableDepResolution: + setPatchStep("disable-dependency-resolution", pg.Plan.Spec.stepMap[stepKey]) + case stepConfigurationPackageEnableDepResolution: + setPatchStep("enable-dependency-resolution", pg.Plan.Spec.stepMap[stepKey]) + case stepEditConfigurationPackage: + setPatchStep("edit-configuration-package", pg.Plan.Spec.stepMap[stepKey]) + case stepEditPackageLock: + setPatchStep("edit-package-lock", pg.Plan.Spec.stepMap[stepKey]) + case stepDeleteMonolithicProvider: + setDeleteStep("delete-monolithic-provider", pg.Plan.Spec.stepMap[stepKey]) + case stepActivateServiceScopedProviderRevision: + setPatchStep("activate-ssop", pg.Plan.Spec.stepMap[stepKey]) + case stepEditConfigurationMetadata: + setPatchStep("edit-configuration-metadata", pg.Plan.Spec.stepMap[stepKey]) + default: + panic(fmt.Sprintf(errInvalidStepFmt, s)) + } + return pg.Plan.Spec.stepMap[stepKey] +} + +func (pg *PlanGenerator) stepEditConfigurationMetadata(source UnstructuredWithMetadata, target *UnstructuredWithMetadata) error { + s := pg.stepConfiguration(stepEditConfigurationMetadata) + target.Metadata.Path = fmt.Sprintf("%s/%s.yaml", s.Name, getVersionedName(target.Object)) + s.Patch.Files = append(s.Patch.Files, target.Metadata.Path) + patchMap, err := computeJSONMergePathDoc(source.Object, target.Object) + if err != nil { + return err + } + return errors.Wrap(pg.target.Put(UnstructuredWithMetadata{ + Object: unstructured.Unstructured{ + Object: addNameGVK(target.Object, patchMap), + }, + Metadata: target.Metadata, + }), errConfigurationMetadataOutput) +} diff --git a/pkg/migration/configurationpackage_steps.go b/pkg/migration/configurationpackage_steps.go new file mode 100644 index 00000000..e5c5ad76 --- /dev/null +++ b/pkg/migration/configurationpackage_steps.go @@ -0,0 +1,112 @@ +// 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" + + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +const ( + errEditConfigurationPackageFmt = `failed to put the edited Configuration package: %s` +) + +func (pg *PlanGenerator) convertConfigurationPackage(o UnstructuredWithMetadata) error { + pkg, err := toConfigurationPackageV1(o.Object) + if err != nil { + return err + } + + // add step for disabling the dependency resolution + // for the configuration package + s := pg.stepConfiguration(stepConfigurationPackageDisableDepResolution) + p := fmt.Sprintf("%s/%s.yaml", s.Name, getVersionedName(o.Object)) + s.Patch.Files = append(s.Patch.Files, p) + if err := pg.target.Put(UnstructuredWithMetadata{ + Object: unstructured.Unstructured{ + Object: addNameGVK(o.Object, map[string]any{ + "spec": map[string]any{ + "skipDependencyResolution": true, + }, + }), + }, + Metadata: Metadata{ + Path: p, + }, + }); err != nil { + return err + } + + // add step for enabling the dependency resolution + // for the configuration package + s = pg.stepConfiguration(stepConfigurationPackageEnableDepResolution) + p = fmt.Sprintf("%s/%s.yaml", s.Name, getVersionedName(o.Object)) + s.Patch.Files = append(s.Patch.Files, p) + if err := pg.target.Put(UnstructuredWithMetadata{ + Object: unstructured.Unstructured{ + Object: addNameGVK(o.Object, map[string]any{ + "spec": map[string]any{ + "skipDependencyResolution": false, + }, + }), + }, + Metadata: Metadata{ + Path: p, + }, + }); err != nil { + return err + } + + // add the step for editing the configuration package + for _, pkgConv := range pg.registry.configurationPackageConverters { + if pkgConv.re == nil || pkgConv.converter == nil || !pkgConv.re.MatchString(pkg.Spec.Package) { + continue + } + err := pkgConv.converter.ConfigurationPackageV1(pkg) + if err != nil { + return errors.Wrapf(err, "failed to call converter on Configuration package: %s", pkg.Spec.Package) + } + // TODO: if a 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. + target := &UnstructuredWithMetadata{ + Object: ToSanitizedUnstructured(pkg), + Metadata: o.Metadata, + } + if err := pg.stepEditConfigurationPackage(o, target); err != nil { + return err + } + } + return nil +} + +func (pg *PlanGenerator) stepEditConfigurationPackage(source UnstructuredWithMetadata, t *UnstructuredWithMetadata) error { + s := pg.stepConfigurationWithSubStep(stepEditConfigurationPackage, true) + t.Metadata.Path = fmt.Sprintf("%s/%s.yaml", s.Name, getVersionedName(t.Object)) + s.Patch.Files = append(s.Patch.Files, t.Metadata.Path) + patchMap, err := computeJSONMergePathDoc(source.Object, t.Object) + if err != nil { + return err + } + return errors.Wrap(pg.target.Put(UnstructuredWithMetadata{ + Object: unstructured.Unstructured{ + Object: addNameGVK(t.Object, patchMap), + }, + Metadata: t.Metadata, + }), errEditConfigurationPackageFmt) +} diff --git a/pkg/migration/converter.go b/pkg/migration/converter.go index e2937c96..e8c0526a 100644 --- a/pkg/migration/converter.go +++ b/pkg/migration/converter.go @@ -16,8 +16,13 @@ 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" + xppkgv1 "github.com/crossplane/crossplane/apis/pkg/v1" "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" @@ -26,7 +31,11 @@ import ( ) const ( - errFromUnstructured = "failed to convert from unstructured.Unstructured to the managed resource type" + errFromUnstructured = "failed to convert from unstructured.Unstructured to the managed resource type" + errFromUnstructuredConfMeta = "failed to convert from unstructured.Unstructured to Crossplane Configuration metadata" + errFromUnstructuredConfPackage = "failed to convert from unstructured.Unstructured to Crossplane Configuration package" + errFromUnstructuredProvider = "failed to convert from unstructured.Unstructured to Crossplane Provider package" + // errFromUnstructuredLock = "failed to convert from unstructured.Unstructured to Crossplane package lock" errToUnstructured = "failed to convert from the managed resource type to unstructured.Unstructured" errRawExtensionUnmarshal = "failed to unmarshal runtime.RawExtension" @@ -147,12 +156,17 @@ func convertToComposition(u map[string]interface{}) (*xpv1.Composition, error) { return c, errors.Wrap(k8sjson.UnmarshalCaseSensitivePreserveInts(buff, c), "failed to unmarshal into a v1.Composition") } -func addNameGVK(u unstructured.Unstructured, target map[string]any) map[string]any { +func addGVK(u unstructured.Unstructured, target map[string]any) map[string]any { if target == nil { target = make(map[string]any) } target["apiVersion"] = u.GetAPIVersion() target["kind"] = u.GetKind() + return target +} + +func addNameGVK(u unstructured.Unstructured, target map[string]any) map[string]any { + target = addGVK(u, target) m := target["metadata"] if m == nil { m = make(map[string]any) @@ -165,3 +179,71 @@ 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 toConfigurationPackageV1(u unstructured.Unstructured) (*xppkgv1.Configuration, error) { + conf := &xppkgv1.Configuration{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, conf); err != nil { + return nil, errors.Wrap(err, errFromUnstructuredConfPackage) + } + return conf, nil +} + +func toConfigurationMetadataV1(u unstructured.Unstructured) (*xpmetav1.Configuration, error) { + conf := &xpmetav1.Configuration{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, conf); err != nil { + return nil, errors.Wrap(err, errFromUnstructuredConfMeta) + } + return conf, nil +} + +func toConfigurationMetadataV1Alpha1(u unstructured.Unstructured) (*xpmetav1alpha1.Configuration, error) { + conf := &xpmetav1alpha1.Configuration{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, conf); err != nil { + return nil, errors.Wrap(err, errFromUnstructuredConfMeta) + } + return conf, nil +} + +func toConfigurationMetadata(u unstructured.Unstructured) (metav1.Object, error) { + var conf metav1.Object + var err error + switch u.GroupVersionKind().Version { + case "v1alpha1": + conf, err = toConfigurationMetadataV1Alpha1(u) + default: + conf, err = toConfigurationMetadataV1(u) + } + return conf, err +} + +func toProviderPackage(u unstructured.Unstructured) (*xppkgv1.Provider, error) { + pkg := &xppkgv1.Provider{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, pkg); err != nil { + return nil, errors.Wrap(err, errFromUnstructuredProvider) + } + return pkg, nil +} + +/*func toPackageLock(u unstructured.Unstructured) (*xppkgv1beta1.Lock, error) { + lock := &xppkgv1beta1.Lock{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, lock); err != nil { + return nil, errors.Wrap(err, errFromUnstructuredLock) + } + return lock, 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..57aaf802 100644 --- a/pkg/migration/interfaces.go +++ b/pkg/migration/interfaces.go @@ -17,6 +17,10 @@ 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" + xppkgv1 "github.com/crossplane/crossplane/apis/pkg/v1" + xppkgv1beta1 "github.com/crossplane/crossplane/apis/pkg/v1beta1" ) // ResourceConverter converts a managed resource from @@ -69,6 +73,44 @@ type PatchSetConverter interface { PatchSets(psMap map[string]*xpv1.PatchSet) error } +// ConfigurationMetadataConverter converts a Crossplane Configuration's metadata. +type ConfigurationMetadataConverter interface { + // ConfigurationMetadataV1 takes a Crossplane Configuration v1 metadata, + // converts it, and stores the converted metadata in its argument. + // Returns any errors encountered during the conversion. + ConfigurationMetadataV1(configuration *xpmetav1.Configuration) error + // ConfigurationMetadataV1Alpha1 takes a Crossplane Configuration v1alpha1 + // metadata, converts it, and stores the converted metadata in its + // argument. Returns any errors encountered during the conversion. + ConfigurationMetadataV1Alpha1(configuration *xpmetav1alpha1.Configuration) error +} + +// ConfigurationPackageConverter converts a Crossplane configuration package. +type ConfigurationPackageConverter interface { + // ConfigurationPackageV1 takes a Crossplane Configuration v1 package, + // converts it possibly to multiple packages and returns + // the converted configuration package. + // Returns any errors encountered during the conversion. + ConfigurationPackageV1(pkg *xppkgv1.Configuration) error +} + +// ProviderPackageConverter converts a Crossplane provider package. +type ProviderPackageConverter interface { + // ProviderPackageV1 takes a Crossplane Provider v1 package, + // converts it possibly to multiple packages and returns the + // converted provider packages. + // Returns any errors encountered during the conversion. + ProviderPackageV1(pkg xppkgv1.Provider) ([]xppkgv1.Provider, error) +} + +// PackageLockConverter converts a Crossplane package lock. +type PackageLockConverter interface { + // PackageLockV1Beta1 takes a Crossplane v1beta1 package lock, + // converts it, and stores the converted lock in its argument. + // Returns any errors encountered during the conversion. + PackageLockV1Beta1(lock *xppkgv1beta1.Lock) 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 +130,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..fc733350 100644 --- a/pkg/migration/plan_generator.go +++ b/pkg/migration/plan_generator.go @@ -17,61 +17,46 @@ package migration import ( "fmt" "reflect" - "strconv" - "strings" "time" - 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" + xpmetav1alpha1 "github.com/crossplane/crossplane/apis/pkg/meta/v1alpha1" + xppkgv1 "github.com/crossplane/crossplane/apis/pkg/v1" + xppkgv1beta1 "github.com/crossplane/crossplane/apis/pkg/v1beta1" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/rand" ) const ( - errSourceHasNext = "failed to generate migration plan: Could not check next object from source" - errSourceNext = "failed to generate migration plan: Could not get next object from source" - errUnstructuredConvert = "failed to convert from unstructured object to v1.Composition" - errUnstructuredMarshal = "failed to marshal unstructured object to JSON" - errResourceMigrate = "failed to migrate resource" - errCompositePause = "failed to pause composite resource" - errCompositesEdit = "failed to edit composite resources" - errCompositesStart = "failed to start composite resources" - errCompositionMigrate = "failed to migrate the composition" - 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" - errResourceOrphan = "failed to orphan managed resource" - errCompositionOutput = "failed to output migrated composition" - errCompositeOutput = "failed to output migrated composite" - errClaimOutput = "failed to output migrated claim" - errClaimsEdit = "failed to edit claims" - errPlanGeneration = "failed to generate the migration plan" - errPause = "failed to store a paused manifest" - errMissingGVK = "managed resource is missing its GVK. Resource converters must set GVKs on any managed resources they newly generate." -) - -type step int - -const ( - stepPauseManaged step = iota - stepPauseComposites - stepCreateNewManaged - stepNewCompositions - stepEditComposites - stepEditClaims - stepDeletionPolicyOrphan - stepDeleteOldManaged - stepStartManaged - stepStartComposites + errSourceHasNext = "failed to generate migration plan: Could not check next object from source" + errSourceNext = "failed to generate migration plan: Could not get next object from source" + errUnstructuredConvert = "failed to convert from unstructured object to v1.Composition" + errUnstructuredMarshal = "failed to marshal unstructured object to JSON" + errResourceMigrate = "failed to migrate resource" + errCompositePause = "failed to pause composite resource" + errCompositesEdit = "failed to edit composite resources" + errCompositesStart = "failed to start composite resources" + errCompositionMigrateFmt = "failed to migrate the composition: %s" + errConfigurationMetadataMigrateFmt = "failed to migrate the configuration metadata: %s" + errConfigurationPackageMigrateFmt = "failed to migrate the configuration package: %s" + errProviderMigrateFmt = "failed to migrate the Provider package: %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" + errResourceOrphan = "failed to orphan managed resource" + errCompositionOutput = "failed to output migrated composition" + errCompositeOutput = "failed to output migrated composite" + errClaimOutput = "failed to output migrated claim" + errClaimsEdit = "failed to edit claims" + errPlanGeneration = "failed to generate the migration plan" + errPause = "failed to store a paused manifest" + errMissingGVK = "managed resource is missing its GVK. Resource converters must set GVKs on any managed resources they newly generate." ) const ( @@ -110,6 +95,7 @@ type PlanGenerator struct { source Source target Target registry *Registry + subSteps map[step]string // Plan is the migration.Plan whose steps are expected // to complete a migration when they're executed in order. Plan Plan @@ -132,6 +118,7 @@ func NewPlanGenerator(registry *Registry, source Source, target Target, opts ... source: source, target: target, registry: registry, + subSteps: map[step]string{}, } for _, o := range opts { o(pg) @@ -145,7 +132,9 @@ func NewPlanGenerator(registry *Registry, source Source, target Target, opts ... // PlanGenerator.Plan variable if the generation is successful // (i.e., no errors are reported). func (pg *PlanGenerator) GeneratePlan() error { - pg.buildPlan() + pg.Plan.Spec.stepMap = make(map[string]*Step) + pg.Plan.Version = versionV010 + defer pg.commitSteps() return errors.Wrap(pg.convert(), errPlanGeneration) } @@ -197,19 +186,44 @@ func (pg *PlanGenerator) convert() error { //nolint: gocyclo return errors.Wrap(err, errSourceNext) } switch gvk := o.Object.GroupVersionKind(); gvk { + case xppkgv1.ConfigurationGroupVersionKind: + if err := pg.convertConfigurationPackage(o); err != nil { + return errors.Wrapf(err, errConfigurationPackageMigrateFmt, o.Object.GetName()) + } + case xpmetav1.ConfigurationGroupVersionKind, xpmetav1alpha1.ConfigurationGroupVersionKind: + target, converted, err := pg.convertConfigurationMetadata(o) + if err != nil { + return errors.Wrapf(err, errConfigurationMetadataMigrateFmt, o.Object.GetName()) + } + if converted { + if err := pg.stepEditConfigurationMetadata(o, target); err != nil { + return err + } + } 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()) } } + case xppkgv1.ProviderGroupVersionKind: + isConverted, err := pg.convertProviderPackage(o) + if err != nil { + return errors.Wrap(err, errProviderMigrateFmt) + } + if isConverted { + if err := pg.stepDeleteMonolith(o); err != nil { + return err + } + } + case xppkgv1beta1.LockGroupVersionKind: default: if o.Metadata.Category == CategoryComposite { if err := pg.stepPauseComposite(&o); err != nil { @@ -314,22 +328,6 @@ 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) - } - mg, ok := obj.(resource.Managed) - return mg, ok, nil -} - func (pg *PlanGenerator) convertComposition(o UnstructuredWithMetadata) (*UnstructuredWithMetadata, bool, error) { // nolint:gocyclo convertedPS, err := pg.convertPatchSets(o) if err != nil { @@ -344,7 +342,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{ @@ -419,251 +417,6 @@ func (pg *PlanGenerator) setDefaultsOnTargetTemplate(sourceName *string, sourceN return nil } -// NOTE: to cover different migration scenarios, we may use -// "migration templates" instead of a static plan. But a static plan should be -// fine as a start. -func (pg *PlanGenerator) buildPlan() { - pg.Plan.Spec.Steps = make([]Step, 10) - - pg.Plan.Spec.Steps[stepPauseManaged].Name = "pause-managed" - pg.Plan.Spec.Steps[stepPauseManaged].Type = StepTypePatch - pg.Plan.Spec.Steps[stepPauseManaged].Patch = &PatchStep{} - pg.Plan.Spec.Steps[stepPauseManaged].Patch.Type = PatchTypeMerge - - pg.Plan.Spec.Steps[stepPauseComposites].Name = "pause-composites" - pg.Plan.Spec.Steps[stepPauseComposites].Type = StepTypePatch - pg.Plan.Spec.Steps[stepPauseComposites].Patch = &PatchStep{} - pg.Plan.Spec.Steps[stepPauseComposites].Patch.Type = PatchTypeMerge - - pg.Plan.Spec.Steps[stepCreateNewManaged].Name = "create-new-managed" - pg.Plan.Spec.Steps[stepCreateNewManaged].Type = StepTypeApply - pg.Plan.Spec.Steps[stepCreateNewManaged].Apply = &ApplyStep{} - - pg.Plan.Spec.Steps[stepNewCompositions].Name = "new-compositions" - pg.Plan.Spec.Steps[stepNewCompositions].Type = StepTypeApply - pg.Plan.Spec.Steps[stepNewCompositions].Apply = &ApplyStep{} - - pg.Plan.Spec.Steps[stepEditComposites].Name = "edit-composites" - pg.Plan.Spec.Steps[stepEditComposites].Type = StepTypePatch - pg.Plan.Spec.Steps[stepEditComposites].Patch = &PatchStep{} - pg.Plan.Spec.Steps[stepEditComposites].Patch.Type = PatchTypeMerge - - pg.Plan.Spec.Steps[stepEditClaims].Name = "edit-claims" - pg.Plan.Spec.Steps[stepEditClaims].Type = StepTypePatch - pg.Plan.Spec.Steps[stepEditClaims].Patch = &PatchStep{} - pg.Plan.Spec.Steps[stepEditClaims].Patch.Type = PatchTypeMerge - - pg.Plan.Spec.Steps[stepDeletionPolicyOrphan].Name = "deletion-policy-orphan" - pg.Plan.Spec.Steps[stepDeletionPolicyOrphan].Type = StepTypePatch - pg.Plan.Spec.Steps[stepDeletionPolicyOrphan].Patch = &PatchStep{} - pg.Plan.Spec.Steps[stepDeletionPolicyOrphan].Patch.Type = PatchTypeMerge - - pg.Plan.Spec.Steps[stepDeleteOldManaged].Name = "delete-old-managed" - pg.Plan.Spec.Steps[stepDeleteOldManaged].Type = StepTypeDelete - deletePolicy := FinalizerPolicyRemove - pg.Plan.Spec.Steps[stepDeleteOldManaged].Delete = &DeleteStep{ - Options: &DeleteOptions{ - FinalizerPolicy: &deletePolicy, - }, - } - - pg.Plan.Spec.Steps[stepStartManaged].Name = "start-managed" - pg.Plan.Spec.Steps[stepStartManaged].Type = StepTypePatch - pg.Plan.Spec.Steps[stepStartManaged].Patch = &PatchStep{} - pg.Plan.Spec.Steps[stepStartManaged].Patch.Type = PatchTypeMerge - - pg.Plan.Spec.Steps[stepStartComposites].Name = "start-composites" - pg.Plan.Spec.Steps[stepStartComposites].Type = StepTypePatch - pg.Plan.Spec.Steps[stepStartComposites].Patch = &PatchStep{} - pg.Plan.Spec.Steps[stepStartComposites].Patch.Type = PatchTypeMerge - pg.Plan.Version = versionV010 -} - -func (pg *PlanGenerator) addStepsForManagedResource(u *UnstructuredWithMetadata) error { - if _, ok, err := toManagedResource(pg.registry.scheme, u.Object); err != nil || !ok { - // not a managed resource or unable to determine - // whether it's a managed resource - return nil // nolint:nilerr - } - qName := getQualifiedName(u.Object) - if err := pg.stepPauseManagedResource(u, qName); err != nil { - return err - } - if err := pg.stepOrphanManagedResource(u, qName); err != nil { - return err - } - pg.stepDeleteOldManagedResource(u) - return nil -} - -func (pg *PlanGenerator) stepStartManagedResource(u *UnstructuredWithMetadata) error { - u.Metadata.Path = fmt.Sprintf("%s/%s.yaml", pg.Plan.Spec.Steps[stepStartManaged].Name, getQualifiedName(u.Object)) - pg.Plan.Spec.Steps[stepStartManaged].Patch.Files = append(pg.Plan.Spec.Steps[stepStartManaged].Patch.Files, u.Metadata.Path) - return pg.pause(*u, false) -} - -func (pg *PlanGenerator) stepPauseManagedResource(u *UnstructuredWithMetadata, qName string) error { - u.Metadata.Path = fmt.Sprintf("%s/%s.yaml", pg.Plan.Spec.Steps[stepPauseManaged].Name, qName) - pg.Plan.Spec.Steps[stepPauseManaged].Patch.Files = append(pg.Plan.Spec.Steps[stepPauseManaged].Patch.Files, u.Metadata.Path) - return pg.pause(*u, true) -} - -func (pg *PlanGenerator) stepPauseComposite(u *UnstructuredWithMetadata) error { - u.Metadata.Path = fmt.Sprintf("%s/%s.yaml", pg.Plan.Spec.Steps[stepPauseComposites].Name, getQualifiedName(u.Object)) - pg.Plan.Spec.Steps[stepPauseComposites].Patch.Files = append(pg.Plan.Spec.Steps[stepPauseComposites].Patch.Files, u.Metadata.Path) - return pg.pause(*u, true) -} - -func (pg *PlanGenerator) stepOrphanManagedResource(u *UnstructuredWithMetadata, qName string) error { - u.Metadata.Path = fmt.Sprintf("%s/%s.yaml", pg.Plan.Spec.Steps[stepDeletionPolicyOrphan].Name, qName) - pg.Plan.Spec.Steps[stepDeletionPolicyOrphan].Patch.Files = append(pg.Plan.Spec.Steps[stepDeletionPolicyOrphan].Patch.Files, u.Metadata.Path) - return errors.Wrap(pg.target.Put(UnstructuredWithMetadata{ - Object: unstructured.Unstructured{ - Object: addNameGVK(u.Object, map[string]any{ - "spec": map[string]any{ - "deletionPolicy": string(v1.DeletionOrphan), - }, - }), - }, - Metadata: u.Metadata, - }), errResourceOrphan) -} - -func (pg *PlanGenerator) stepDeleteOldManagedResource(u *UnstructuredWithMetadata) { - pg.Plan.Spec.Steps[stepDeleteOldManaged].Delete.Resources = append(pg.Plan.Spec.Steps[stepDeleteOldManaged].Delete.Resources, - Resource{ - GroupVersionKind: FromGroupVersionKind(u.Object.GroupVersionKind()), - Name: u.Object.GetName(), - }) -} - -func (pg *PlanGenerator) pause(u UnstructuredWithMetadata, isPaused bool) error { - return errors.Wrap(pg.target.Put(UnstructuredWithMetadata{ - Object: unstructured.Unstructured{ - Object: addNameGVK(u.Object, map[string]any{ - "metadata": map[string]any{ - "annotations": map[string]any{ - meta.AnnotationKeyReconciliationPaused: strconv.FormatBool(isPaused), - }, - }, - }), - }, - Metadata: Metadata{ - Path: u.Metadata.Path, - }, - }), errPause) -} - -func getQualifiedName(u unstructured.Unstructured) string { - namePrefix := u.GetName() - if len(namePrefix) == 0 { - namePrefix = fmt.Sprintf("%s%s", u.GetGenerateName(), rand.String(5)) - } - gvk := u.GroupVersionKind() - return fmt.Sprintf("%s.%ss.%s", namePrefix, strings.ToLower(gvk.Kind), gvk.Group) -} - -func (pg *PlanGenerator) stepNewManagedResource(u *UnstructuredWithMetadata) error { - meta.AddAnnotations(&u.Object, map[string]string{meta.AnnotationKeyReconciliationPaused: "true"}) - u.Metadata.Path = fmt.Sprintf("%s/%s.yaml", pg.Plan.Spec.Steps[stepCreateNewManaged].Name, getQualifiedName(u.Object)) - pg.Plan.Spec.Steps[stepCreateNewManaged].Apply.Files = append(pg.Plan.Spec.Steps[stepCreateNewManaged].Apply.Files, u.Metadata.Path) - if err := pg.target.Put(*u); err != nil { - return errors.Wrap(err, errResourceOutput) - } - return nil -} - -func (pg *PlanGenerator) stepNewComposition(u *UnstructuredWithMetadata) error { - u.Metadata.Path = fmt.Sprintf("%s/%s.yaml", pg.Plan.Spec.Steps[stepNewCompositions].Name, getQualifiedName(u.Object)) - pg.Plan.Spec.Steps[stepNewCompositions].Apply.Files = append(pg.Plan.Spec.Steps[stepNewCompositions].Apply.Files, u.Metadata.Path) - if err := pg.target.Put(*u); err != nil { - return errors.Wrap(err, errCompositionOutput) - } - return nil -} - -func (pg *PlanGenerator) stepStartComposites(composites []UnstructuredWithMetadata) error { - for _, u := range composites { - u.Metadata.Path = fmt.Sprintf("%s/%s.yaml", pg.Plan.Spec.Steps[stepStartComposites].Name, getQualifiedName(u.Object)) - pg.Plan.Spec.Steps[stepStartComposites].Patch.Files = append(pg.Plan.Spec.Steps[stepStartComposites].Patch.Files, u.Metadata.Path) - if err := pg.pause(u, false); err != nil { - return errors.Wrap(err, errCompositeOutput) - } - } - return nil -} - -func (pg *PlanGenerator) stepEditComposites(composites []UnstructuredWithMetadata, convertedMap map[corev1.ObjectReference][]UnstructuredWithMetadata, convertedComposition map[string]string) error { - for _, u := range composites { - cp := composite.Unstructured{Unstructured: u.Object} - refs := cp.GetResourceReferences() - // compute new spec.resourceRefs so that the XR references the new MRs - newRefs := make([]corev1.ObjectReference, 0, len(refs)) - for _, ref := range refs { - converted, ok := convertedMap[ref] - if !ok { - newRefs = append(newRefs, ref) - continue - } - for _, o := range converted { - gvk := o.Object.GroupVersionKind() - newRefs = append(newRefs, corev1.ObjectReference{ - Kind: gvk.Kind, - Name: o.Object.GetName(), - APIVersion: gvk.GroupVersion().String(), - }) - } - } - cp.SetResourceReferences(newRefs) - // compute new spec.compositionRef - if ref := cp.GetCompositionReference(); ref != nil && convertedComposition[ref.Name] != "" { - ref.Name = convertedComposition[ref.Name] - cp.SetCompositionReference(ref) - } - spec := u.Object.Object["spec"].(map[string]any) - u.Metadata.Path = fmt.Sprintf("%s/%s.yaml", pg.Plan.Spec.Steps[stepEditComposites].Name, getQualifiedName(u.Object)) - pg.Plan.Spec.Steps[stepEditComposites].Patch.Files = append(pg.Plan.Spec.Steps[stepEditComposites].Patch.Files, u.Metadata.Path) - if err := pg.target.Put(UnstructuredWithMetadata{ - Object: unstructured.Unstructured{ - Object: addNameGVK(u.Object, map[string]any{ - "spec": map[string]any{ - keyResourceRefs: spec[keyResourceRefs], - keyCompositionRef: spec[keyCompositionRef]}, - }), - }, - Metadata: u.Metadata, - }); err != nil { - return errors.Wrap(err, errCompositeOutput) - } - } - return nil -} - -func (pg *PlanGenerator) stepEditClaims(claims []UnstructuredWithMetadata, convertedComposition map[string]string) error { - for _, u := range claims { - cm := claim.Unstructured{Unstructured: u.Object} - if ref := cm.GetCompositionReference(); ref != nil && convertedComposition[ref.Name] != "" { - ref.Name = convertedComposition[ref.Name] - cm.SetCompositionReference(ref) - } - u.Metadata.Path = fmt.Sprintf("%s/%s.yaml", pg.Plan.Spec.Steps[stepEditClaims].Name, getQualifiedName(u.Object)) - pg.Plan.Spec.Steps[stepEditClaims].Patch.Files = append(pg.Plan.Spec.Steps[stepEditClaims].Patch.Files, u.Metadata.Path) - if err := pg.target.Put(UnstructuredWithMetadata{ - Object: unstructured.Unstructured{ - Object: addNameGVK(u.Object, map[string]any{ - "spec": map[string]any{ - keyCompositionRef: u.Object.Object["spec"].(map[string]any)[keyCompositionRef], - }, - }), - }, - Metadata: u.Metadata, - }); err != nil { - return errors.Wrap(err, errClaimOutput) - } - } - return nil -} - func init() { rand.Seed(time.Now().UnixNano()) } diff --git a/pkg/migration/plan_generator_test.go b/pkg/migration/plan_generator_test.go index d240db27..7bce05c3 100644 --- a/pkg/migration/plan_generator_test.go +++ b/pkg/migration/plan_generator_test.go @@ -18,11 +18,21 @@ import ( "bytes" "os" "path/filepath" + "regexp" "testing" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + xppkgv1 "github.com/crossplane/crossplane/apis/pkg/v1" + + "github.com/google/go-cmp/cmp/cmpopts" + + 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" @@ -49,6 +59,14 @@ func TestGeneratePlan(t *testing.T) { fields fields want want }{ + "EmptyPlan": { + fields: fields{ + source: newTestSource(map[string]Metadata{}), + target: newTestTarget(), + registry: getRegistryWithConverters(nil, nil, nil, nil, nil), + }, + want: want{}, + }, "PlanWithManagedResourceAndClaim": { fields: fields{ source: newTestSource(map[string]Metadata{ @@ -95,7 +113,7 @@ func TestGeneratePlan(t *testing.T) { re: AllCompositions, converter: &testConverter{}, }, - }), + }, nil, nil, nil), }, want: want{ migrationPlanPath: "testdata/plan/generated/migration_plan.yaml", @@ -112,6 +130,120 @@ func TestGeneratePlan(t *testing.T) { }, }, }, + "PlanWithConfigurationV1": { + fields: fields{ + source: newTestSource(map[string]Metadata{ + "testdata/plan/configurationv1.yaml": {}}), + target: newTestTarget(), + registry: getRegistryWithConverters(nil, nil, []configurationMetadataConverter{ + { + re: AllConfigurations, + converter: &configurationMetaTestConverter{}, + }, + }, nil, nil), + }, + want: want{ + migrationPlanPath: "testdata/plan/generated/configurationv1_migration_plan.yaml", + migratedResourceNames: []string{ + "edit-configuration-metadata/platform-ref-aws.configurations.meta.pkg.crossplane.io_v1.yaml", + }, + }, + }, + "PlanWithConfigurationV1Alpha1": { + fields: fields{ + source: newTestSource(map[string]Metadata{ + "testdata/plan/configurationv1alpha1.yaml": {}}), + target: newTestTarget(), + registry: getRegistryWithConverters(nil, nil, []configurationMetadataConverter{ + { + re: AllConfigurations, + converter: &configurationMetaTestConverter{}, + }, + }, nil, nil), + }, + want: want{ + migrationPlanPath: "testdata/plan/generated/configurationv1alpha1_migration_plan.yaml", + migratedResourceNames: []string{ + "edit-configuration-metadata/platform-ref-aws.configurations.meta.pkg.crossplane.io_v1alpha1.yaml", + }, + }, + }, + "PlanWithProviderPackageV1": { + fields: fields{ + source: newTestSource(map[string]Metadata{ + "testdata/plan/providerv1.yaml": {}}), + target: newTestTarget(), + registry: getRegistryWithConverters(nil, nil, nil, nil, + []providerPackageConverter{ + { + re: regexp.MustCompile(`xpkg.upbound.io/upbound/provider-aws:.+`), + converter: &monolithProviderToFamilyConfigConverter{}, + }, + { + re: regexp.MustCompile(`xpkg.upbound.io/upbound/provider-aws:.+`), + converter: &monolithicProviderToSSOPConverter{}, + }, + }), + }, + want: want{ + migrationPlanPath: "testdata/plan/generated/providerv1_migration_plan.yaml", + migratedResourceNames: []string{ + "new-ssop/provider-family-aws.providers.pkg.crossplane.io_v1.yaml", + "new-ssop/provider-aws-ec2.providers.pkg.crossplane.io_v1.yaml", + "new-ssop/provider-aws-eks.providers.pkg.crossplane.io_v1.yaml", + "activate-ssop/provider-family-aws.providers.pkg.crossplane.io_v1.yaml", + "activate-ssop/provider-aws-ec2.providers.pkg.crossplane.io_v1.yaml", + "activate-ssop/provider-aws-eks.providers.pkg.crossplane.io_v1.yaml", + }, + }, + }, + "PlanWithProviderPackageV1AndConfigurationV1": { + fields: fields{ + source: newTestSource(map[string]Metadata{ + "testdata/plan/providerv1.yaml": {}, + "testdata/plan/configurationv1.yaml": {}, + "testdata/plan/configurationpkgv1.yaml": {}, + }), + target: newTestTarget(), + registry: getRegistryWithConverters(nil, nil, + []configurationMetadataConverter{ + { + re: AllConfigurations, + converter: &configurationMetaTestConverter{}, + }, + }, []configurationPackageConverter{ + { + re: regexp.MustCompile(`xpkg.upbound.io/upbound/provider-ref-aws:.+`), + converter: &configurationPackageTestConverter{}, + }, + }, + []providerPackageConverter{ + { + re: regexp.MustCompile(`xpkg.upbound.io/upbound/provider-aws:.+`), + converter: &monolithProviderToFamilyConfigConverter{}, + }, + { + re: regexp.MustCompile(`xpkg.upbound.io/upbound/provider-aws:.+`), + converter: &monolithicProviderToSSOPConverter{}, + }, + }), + }, + want: want{ + migrationPlanPath: "testdata/plan/generated/configurationv1_providerv1_migration_plan.yaml", + migratedResourceNames: []string{ + "disable-dependency-resolution/platform-ref-aws.configurations.pkg.crossplane.io_v1.yaml", + "edit-configuration-package/platform-ref-aws.configurations.pkg.crossplane.io_v1.yaml", + "enable-dependency-resolution/platform-ref-aws.configurations.pkg.crossplane.io_v1.yaml", + "edit-configuration-metadata/platform-ref-aws.configurations.meta.pkg.crossplane.io_v1.yaml", + "new-ssop/provider-family-aws.providers.pkg.crossplane.io_v1.yaml", + "new-ssop/provider-aws-ec2.providers.pkg.crossplane.io_v1.yaml", + "new-ssop/provider-aws-eks.providers.pkg.crossplane.io_v1.yaml", + "activate-ssop/provider-family-aws.providers.pkg.crossplane.io_v1.yaml", + "activate-ssop/provider-aws-ec2.providers.pkg.crossplane.io_v1.yaml", + "activate-ssop/provider-aws-eks.providers.pkg.crossplane.io_v1.yaml", + }, + }, + }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { @@ -129,7 +261,7 @@ func TestGeneratePlan(t *testing.T) { if err != nil { t.Fatalf("Failed to load plan file from path %s: %v", tt.want.migrationPlanPath, err) } - if diff := cmp.Diff(p, &pg.Plan); diff != "" { + if diff := cmp.Diff(p, &pg.Plan, cmpopts.IgnoreUnexported(Spec{})); diff != "" { t.Errorf("GeneratePlan(): -wantPlan, +gotPlan: %s", diff) } // compare generated migration files with the expected ones @@ -249,7 +381,8 @@ 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, confMetaConverters []configurationMetadataConverter, confPackageConverters []configurationPackageConverter, + providerPkgConverters []providerPackageConverter) *Registry { scheme := runtime.NewScheme() scheme.AddKnownTypeWithName(fake.MigrationSourceGVK, &fake.MigrationSourceObject{}) scheme.AddKnownTypeWithName(fake.MigrationTargetGVK, &fake.MigrationTargetObject{}) @@ -257,13 +390,25 @@ func getRegistryWithConverters(converters map[schema.GroupVersionKind]delegating for _, c := range psConverters { r.RegisterPatchSetConverter(c.re, c.converter) } + for _, c := range confMetaConverters { + r.RegisterConfigurationConverter(c.re, c.converter) + } + for _, c := range confPackageConverters { + r.RegisterConfigurationPackageConverter(c.re, c.converter) + } + for _, c := range providerPkgConverters { + r.RegisterProviderPackageConverter(c.re, c.converter) + } for gvk, d := range converters { - r.RegisterConversionFunctions(gvk, d.rFn, d.cmpFn, nil) + r.RegisterAPIConversionFunctions(gvk, d.rFn, d.cmpFn, nil) } return r } func loadPlan(planPath string) (*Plan, error) { + if planPath == "" { + return emptyPlan(), nil + } buff, err := os.ReadFile(planPath) if err != nil { return nil, err @@ -271,3 +416,87 @@ func loadPlan(planPath string) (*Plan, error) { p := &Plan{} return p, k8syaml.Unmarshal(buff, p) } + +func emptyPlan() *Plan { + return &Plan{ + Version: versionV010, + } +} + +type configurationPackageTestConverter struct{} + +func (c *configurationPackageTestConverter) ConfigurationPackageV1(pkg *xppkgv1.Configuration) error { + pkg.Spec.Package = "xpkg.upbound.io/upbound/provider-ref-aws:v0.2.0-ssop" + return nil +} + +type configurationMetaTestConverter struct{} + +func (cc *configurationMetaTestConverter) ConfigurationMetadataV1(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 (cc *configurationMetaTestConverter) ConfigurationMetadataV1Alpha1(c *xpmetav1alpha1.Configuration) error { + c.Spec.DependsOn = []xpmetav1alpha1.Dependency{ + { + Provider: ptrFromString("xpkg.upbound.io/upbound/provider-aws-eks"), + Version: ">=v0.17.0", + }, + } + return nil +} + +type monolithProviderToFamilyConfigConverter struct{} + +func (c *monolithProviderToFamilyConfigConverter) ProviderPackageV1(_ xppkgv1.Provider) ([]xppkgv1.Provider, error) { + ap := xppkgv1.ManualActivation + return []xppkgv1.Provider{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "provider-family-aws", + }, + Spec: xppkgv1.ProviderSpec{ + PackageSpec: xppkgv1.PackageSpec{ + Package: "xpkg.upbound.io/upbound/provider-family-aws:v0.37.0", + RevisionActivationPolicy: &ap, + }, + }, + }, + }, nil +} + +type monolithicProviderToSSOPConverter struct{} + +func (c *monolithicProviderToSSOPConverter) ProviderPackageV1(_ xppkgv1.Provider) ([]xppkgv1.Provider, error) { + ap := xppkgv1.ManualActivation + return []xppkgv1.Provider{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "provider-aws-ec2", + }, + Spec: xppkgv1.ProviderSpec{ + PackageSpec: xppkgv1.PackageSpec{ + Package: "xpkg.upbound.io/upbound/provider-aws-ec2:v0.37.0", + RevisionActivationPolicy: &ap, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "provider-aws-eks", + }, + Spec: xppkgv1.ProviderSpec{ + PackageSpec: xppkgv1.PackageSpec{ + Package: "xpkg.upbound.io/upbound/provider-aws-eks:v0.37.0", + RevisionActivationPolicy: &ap, + }, + }, + }, + }, nil +} diff --git a/pkg/migration/plan_steps.go b/pkg/migration/plan_steps.go new file mode 100644 index 00000000..c2193382 --- /dev/null +++ b/pkg/migration/plan_steps.go @@ -0,0 +1,115 @@ +// 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" + "sort" + "strings" + + "k8s.io/apimachinery/pkg/util/rand" + + "k8s.io/apimachinery/pkg/util/jsonmergepatch" + + "github.com/pkg/errors" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/util/json" +) + +type step int + +const ( + errMarshalSourceForPatch = "failed to marshal source object for computing JSON merge patch" + errMarshalTargetForPatch = "failed to marshal target object for computing JSON merge patch" + errMergePatch = "failed to compute the JSON merge patch document" + errMergePatchMap = "failed to unmarshal the JSON merge patch document into map" + errInvalidStepFmt = "invalid step ID: %d" +) + +func setApplyStep(name string, s *Step) { + s.Name = name + s.Type = StepTypeApply + s.Apply = &ApplyStep{} +} + +func setPatchStep(name string, s *Step) { + s.Name = name + s.Type = StepTypePatch + s.Patch = &PatchStep{} + s.Patch.Type = PatchTypeMerge +} + +func setDeleteStep(name string, s *Step) { + s.Name = name + s.Type = StepTypeDelete + deletePolicy := FinalizerPolicyRemove + s.Delete = &DeleteStep{ + Options: &DeleteOptions{ + FinalizerPolicy: &deletePolicy, + }, + } +} + +func (pg *PlanGenerator) commitSteps() { + if len(pg.Plan.Spec.stepMap) == 0 { + return + } + pg.Plan.Spec.Steps = make([]Step, 0, len(pg.Plan.Spec.stepMap)) + keys := make([]string, 0, len(pg.Plan.Spec.stepMap)) + for s := range pg.Plan.Spec.stepMap { + keys = append(keys, s) + } + sort.Strings(keys) + for _, s := range keys { + pg.Plan.Spec.Steps = append(pg.Plan.Spec.Steps, *pg.Plan.Spec.stepMap[s]) + } +} + +func computeJSONMergePathDoc(source, target unstructured.Unstructured) (map[string]any, error) { + sourceBuff, err := source.MarshalJSON() + if err != nil { + return nil, errors.Wrap(err, errMarshalSourceForPatch) + } + targetBuff, err := target.MarshalJSON() + if err != nil { + return nil, errors.Wrap(err, errMarshalTargetForPatch) + } + patch, err := jsonmergepatch.CreateThreeWayJSONMergePatch(sourceBuff, targetBuff, sourceBuff) + if err != nil { + return nil, errors.Wrap(err, errMergePatch) + } + + var result map[string]any + return result, errors.Wrap(json.Unmarshal(patch, &result), errMergePatchMap) +} + +func getQualifiedName(u unstructured.Unstructured) string { + namePrefix := u.GetName() + if len(namePrefix) == 0 { + namePrefix = fmt.Sprintf("%s%s", u.GetGenerateName(), rand.String(5)) + } + gvk := u.GroupVersionKind() + return fmt.Sprintf("%s.%ss.%s", namePrefix, strings.ToLower(gvk.Kind), gvk.Group) +} + +func getVersionedName(u unstructured.Unstructured) string { + v := u.GroupVersionKind().Version + qName := getQualifiedName(u) + if v == "" { + return qName + } + return fmt.Sprintf("%s_%s", qName, v) +} diff --git a/pkg/migration/provider_package_steps.go b/pkg/migration/provider_package_steps.go new file mode 100644 index 00000000..1aa49d5e --- /dev/null +++ b/pkg/migration/provider_package_steps.go @@ -0,0 +1,113 @@ +// 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" + + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +const ( + errPutSSOPPackageFmt = "failed to put the SSOP package: %s" + errActivateSSOP = "failed to put the activated SSOP package: %s" +) + +func (pg *PlanGenerator) convertProviderPackage(o UnstructuredWithMetadata) (bool, error) { + pkg, err := toProviderPackage(o.Object) + if err != nil { + return false, err + } + isConverted := false + for _, pkgConv := range pg.registry.providerPackageConverters { + if pkgConv.re == nil || pkgConv.converter == nil || !pkgConv.re.MatchString(pkg.Spec.Package) { + continue + } + targetPkgs, err := pkgConv.converter.ProviderPackageV1(*pkg) + if err != nil { + return false, errors.Wrapf(err, "failed to call converter on Provider package: %s", pkg.Spec.Package) + } + // 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 + converted := make([]*UnstructuredWithMetadata, 0, len(targetPkgs)) + for _, p := range targetPkgs { + p := p + converted = append(converted, &UnstructuredWithMetadata{ + Object: ToSanitizedUnstructured(&p), + Metadata: o.Metadata, + }) + } + if err := pg.stepNewSSOPs(o, converted); err != nil { + return false, err + } + if err := pg.stepActivateSSOPs(converted); err != nil { + return false, err + } + } + return isConverted, nil +} + +func (pg *PlanGenerator) stepDeleteMonolith(source UnstructuredWithMetadata) error { + // delete the monolithic provider package + s := pg.stepConfiguration(stepDeleteMonolithicProvider) + source.Metadata.Path = fmt.Sprintf("%s/%s.yaml", s.Name, getVersionedName(source.Object)) + s.Delete.Resources = []Resource{ + { + GroupVersionKind: FromGroupVersionKind(source.Object.GroupVersionKind()), + Name: source.Object.GetName(), + }, + } + return nil +} + +// add steps for the new SSOPs +func (pg *PlanGenerator) stepNewSSOPs(source UnstructuredWithMetadata, targets []*UnstructuredWithMetadata) error { + s := pg.stepConfigurationWithSubStep(stepNewServiceScopedProvider, true) + for _, t := range targets { + t.Object.Object = addGVK(source.Object, t.Object.Object) + t.Metadata.Path = fmt.Sprintf("%s/%s.yaml", s.Name, getVersionedName(t.Object)) + s.Apply.Files = append(s.Apply.Files, t.Metadata.Path) + if err := pg.target.Put(*t); err != nil { + return errors.Wrapf(err, errPutSSOPPackageFmt, t.Metadata.Path) + } + } + return nil +} + +// add steps for activating SSOPs +func (pg *PlanGenerator) stepActivateSSOPs(targets []*UnstructuredWithMetadata) error { + s := pg.stepConfigurationWithSubStep(stepActivateServiceScopedProviderRevision, true) + for _, t := range targets { + t.Metadata.Path = fmt.Sprintf("%s/%s.yaml", s.Name, getVersionedName(t.Object)) + s.Patch.Files = append(s.Patch.Files, t.Metadata.Path) + if err := pg.target.Put(UnstructuredWithMetadata{ + Object: unstructured.Unstructured{ + Object: addNameGVK(t.Object, map[string]any{ + "spec": map[string]any{ + "revisionActivationPolicy": "Automatic", + }, + }), + }, + Metadata: t.Metadata, + }); err != nil { + return errors.Wrapf(err, errActivateSSOP, t.Metadata.Path) + } + } + return nil +} diff --git a/pkg/migration/registry.go b/pkg/migration/registry.go index a1f022b2..a27244ae 100644 --- a/pkg/migration/registry.go +++ b/pkg/migration/registry.go @@ -19,6 +19,10 @@ 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" + xppkgv1 "github.com/crossplane/crossplane/apis/pkg/v1" + xppkgv1beta1 "github.com/crossplane/crossplane/apis/pkg/v1beta1" "github.com/pkg/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -27,6 +31,8 @@ import ( var ( // AllCompositions matches all v1.Composition names. AllCompositions = regexp.MustCompile(`.*`) + // AllConfigurations matches all metav1.Configuration names. + AllConfigurations = regexp.MustCompile(`.*`) ) const ( @@ -45,16 +51,50 @@ type patchSetConverter struct { converter PatchSetConverter } +type configurationMetadataConverter 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 ConfigurationMetadataConverter to be run on the Configuration's + // metadata. + converter ConfigurationMetadataConverter +} + +type configurationPackageConverter struct { + // re is the regular expression against which a Configuration package's + // reference will be matched to determine whether the conversion function + // will be invoked. + re *regexp.Regexp + // converter is the ConfigurationPackageConverter to be run on the + // Configuration package. + converter ConfigurationPackageConverter +} + +type providerPackageConverter struct { + // re is the regular expression against which a Provider package's + // reference will be matched to determine whether the conversion function + // will be invoked. + re *regexp.Regexp + // converter is the ProviderPackageConverter to be run on the + // Provider package. + converter ProviderPackageConverter +} + // 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 []configurationMetadataConverter + configurationPackageConverters []configurationPackageConverter + providerPackageConverters []providerPackageConverter + packageLockConverters []PackageLockConverter + scheme *runtime.Scheme + claimTypes []schema.GroupVersionKind + compositeTypes []schema.GroupVersionKind } // NewRegistry returns a new Registry initialized with @@ -102,7 +142,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 +151,83 @@ func (r *Registry) RegisterPatchSetConverter(re *regexp.Regexp, psConv PatchSetC }) } +// RegisterConfigurationConverter registers the given ConfigurationMetadataConverter +// for the configurations whose name match the given regular expression. +func (r *Registry) RegisterConfigurationConverter(re *regexp.Regexp, confConv ConfigurationMetadataConverter) { + r.configurationConverters = append(r.configurationConverters, configurationMetadataConverter{ + re: re, + converter: confConv, + }) +} + +// RegisterConfigurationMetadataV1ConversionFunction registers the specified +// ConfigurationMetadataV1ConversionFn for the v1 configurations whose name match +// the given regular expression. +func (r *Registry) RegisterConfigurationMetadataV1ConversionFunction(re *regexp.Regexp, confConversionFn ConfigurationMetadataV1ConversionFn) { + r.RegisterConfigurationConverter(re, &delegatingConverter{ + confMetaV1Fn: confConversionFn, + }) +} + +// RegisterConfigurationMetadataV1Alpha1ConversionFunction registers the specified +// ConfigurationMetadataV1Alpha1ConversionFn for the v1alpha1 configurations +// whose name match the given regular expression. +func (r *Registry) RegisterConfigurationMetadataV1Alpha1ConversionFunction(re *regexp.Regexp, confConversionFn ConfigurationMetadataV1Alpha1ConversionFn) { + r.RegisterConfigurationConverter(re, &delegatingConverter{ + confMetaV1Alpha1Fn: confConversionFn, + }) +} + +// RegisterConfigurationPackageConverter registers the specified +// ConfigurationPackageConverter for the Configuration v1 packages whose reference +// match the given regular expression. +func (r *Registry) RegisterConfigurationPackageConverter(re *regexp.Regexp, pkgConv ConfigurationPackageConverter) { + r.configurationPackageConverters = append(r.configurationPackageConverters, configurationPackageConverter{ + re: re, + converter: pkgConv, + }) +} + +// RegisterConfigurationPackageV1ConversionFunction registers the specified +// ConfigurationPackageV1ConversionFn for the Configuration v1 packages whose reference +// match the given regular expression. +func (r *Registry) RegisterConfigurationPackageV1ConversionFunction(re *regexp.Regexp, confConversionFn ConfigurationPackageV1ConversionFn) { + r.RegisterConfigurationPackageConverter(re, &delegatingConverter{ + confPackageV1Fn: confConversionFn, + }) +} + +// RegisterProviderPackageConverter registers the given ProviderPackageConverter +// for the provider packages whose references match the given regular expression. +func (r *Registry) RegisterProviderPackageConverter(re *regexp.Regexp, pkgConv ProviderPackageConverter) { + r.providerPackageConverters = append(r.providerPackageConverters, providerPackageConverter{ + re: re, + converter: pkgConv, + }) +} + +// RegisterProviderPackageV1ConversionFunction registers the specified +// ProviderPackageV1ConversionFn for the provider v1 packages whose reference +// match the given regular expression. +func (r *Registry) RegisterProviderPackageV1ConversionFunction(re *regexp.Regexp, pkgConversionFn ProviderPackageV1ConversionFn) { + r.RegisterProviderPackageConverter(re, &delegatingConverter{ + providerPackageV1Fn: pkgConversionFn, + }) +} + +// RegisterPackageLockConverter registers the given PackageLockConverter. +func (r *Registry) RegisterPackageLockConverter(lockConv PackageLockConverter) { + r.packageLockConverters = append(r.packageLockConverters, lockConv) +} + +// RegisterPackageLockV1Beta1ConversionFunction registers the specified +// RegisterPackageLockV1Beta1ConversionFunction for the package v1beta1 locks. +func (r *Registry) RegisterPackageLockV1Beta1ConversionFunction(lockConversionFn PackageLockV1Beta1ConversionFn) { + r.RegisterPackageLockConverter(&delegatingConverter{ + packageLockV1Beta1Fn: lockConversionFn, + }) +} + // 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 +273,19 @@ func (r *Registry) GetCompositionGVKs() []schema.GroupVersionKind { } // GetAllRegisteredGVKs returns a list of registered GVKs -// including v1.CompositionGroupVersionKind +// including v1.CompositionGroupVersionKind, +// metav1.ConfigurationGroupVersionKind, +// metav1alpha1.ConfigurationGroupVersionKind +// pkg.ConfigurationGroupVersionKind, +// pkg.ProviderGroupVersionKind, +// pkg.LockGroupVersionKind. 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, xpmetav1alpha1.ConfigurationGroupVersionKind, + xppkgv1.ConfigurationGroupVersionKind, xppkgv1.ProviderGroupVersionKind, xppkgv1beta1.LockGroupVersionKind) return gvks } @@ -180,10 +303,75 @@ type ComposedTemplateConversionFn func(sourceTemplate xpv1.ComposedTemplate, con // schema to the migration target provider's schema. type PatchSetsConversionFn func(psMap map[string]*xpv1.PatchSet) error +// ConfigurationMetadataV1ConversionFn is a function that converts the specified +// migration source Configuration v1 metadata to the migration target +// Configuration metadata. +type ConfigurationMetadataV1ConversionFn func(configuration *xpmetav1.Configuration) error + +// ConfigurationMetadataV1Alpha1ConversionFn is a function that converts the specified +// migration source Configuration v1alpha1 metadata to the migration target +// Configuration metadata. +type ConfigurationMetadataV1Alpha1ConversionFn func(configuration *xpmetav1alpha1.Configuration) error + +// PackageLockV1Beta1ConversionFn is a function that converts the specified +// migration source package v1beta1 lock to the migration target +// package lock. +type PackageLockV1Beta1ConversionFn func(pkg *xppkgv1beta1.Lock) error + +// ConfigurationPackageV1ConversionFn is a function that converts the specified +// migration source Configuration v1 package to the migration target +// Configuration package(s). +type ConfigurationPackageV1ConversionFn func(pkg *xppkgv1.Configuration) error + +// ProviderPackageV1ConversionFn is a function that converts the specified +// migration source provider v1 package to the migration target +// Provider package(s). +type ProviderPackageV1ConversionFn func(pkg xppkgv1.Provider) ([]xppkgv1.Provider, error) + type delegatingConverter struct { - rFn ResourceConversionFn - cmpFn ComposedTemplateConversionFn - psFn PatchSetsConversionFn + rFn ResourceConversionFn + cmpFn ComposedTemplateConversionFn + psFn PatchSetsConversionFn + confMetaV1Fn ConfigurationMetadataV1ConversionFn + confMetaV1Alpha1Fn ConfigurationMetadataV1Alpha1ConversionFn + confPackageV1Fn ConfigurationPackageV1ConversionFn + providerPackageV1Fn ProviderPackageV1ConversionFn + packageLockV1Beta1Fn PackageLockV1Beta1ConversionFn +} + +func (d *delegatingConverter) ConfigurationPackageV1(pkg *xppkgv1.Configuration) error { + if d.confPackageV1Fn == nil { + return nil + } + return d.confPackageV1Fn(pkg) +} + +func (d *delegatingConverter) PackageLockV1Beta1(lock *xppkgv1beta1.Lock) error { + if d.packageLockV1Beta1Fn == nil { + return nil + } + return d.packageLockV1Beta1Fn(lock) +} + +func (d *delegatingConverter) ProviderPackageV1(pkg xppkgv1.Provider) ([]xppkgv1.Provider, error) { + if d.providerPackageV1Fn == nil { + return []xppkgv1.Provider{pkg}, nil + } + return d.providerPackageV1Fn(pkg) +} + +func (d *delegatingConverter) ConfigurationMetadataV1(c *xpmetav1.Configuration) error { + if d.confMetaV1Fn == nil { + return nil + } + return d.confMetaV1Fn(c) +} + +func (d *delegatingConverter) ConfigurationMetadataV1Alpha1(c *xpmetav1alpha1.Configuration) error { + if d.confMetaV1Alpha1Fn == nil { + return nil + } + return d.confMetaV1Alpha1Fn(c) } func (d *delegatingConverter) PatchSets(psMap map[string]*xpv1.PatchSet) error { @@ -212,13 +400,13 @@ func (d *delegatingConverter) ComposedTemplate(sourceTemplate xpv1.ComposedTempl return d.cmpFn(sourceTemplate, convertedTemplates...) } -// RegisterConversionFunctions registers the supplied ResourceConversionFn and +// RegisterAPIConversionFunctions registers the supplied ResourceConversionFn and // ComposedTemplateConversionFn for the specified GVK, and the supplied // PatchSetsConversionFn for all the discovered Compositions. // The specified GVK must belong to a Crossplane managed resource type and // the type must already have been registered with this registry's scheme // by calling Registry.AddToScheme. -func (r *Registry) RegisterConversionFunctions(gvk schema.GroupVersionKind, rFn ResourceConversionFn, cmpFn ComposedTemplateConversionFn, psFn PatchSetsConversionFn) { +func (r *Registry) RegisterAPIConversionFunctions(gvk schema.GroupVersionKind, rFn ResourceConversionFn, cmpFn ComposedTemplateConversionFn, psFn PatchSetsConversionFn) { d := &delegatingConverter{ rFn: rFn, cmpFn: cmpFn, @@ -227,3 +415,14 @@ func (r *Registry) RegisterConversionFunctions(gvk schema.GroupVersionKind, rFn r.RegisterPatchSetConverter(AllCompositions, d) r.RegisterCompositionConverter(gvk, d) } + +// RegisterConversionFunctions registers the supplied ResourceConversionFn and +// ComposedTemplateConversionFn for the specified GVK, and the supplied +// PatchSetsConversionFn for all the discovered Compositions. +// The specified GVK must belong to a Crossplane managed resource type and +// the type must already have been registered with this registry's scheme +// by calling Registry.AddToScheme. +// Deprecated: Use RegisterAPIConversionFunctions instead. +func (r *Registry) RegisterConversionFunctions(gvk schema.GroupVersionKind, rFn ResourceConversionFn, cmpFn ComposedTemplateConversionFn, psFn PatchSetsConversionFn) { + r.RegisterAPIConversionFunctions(gvk, rFn, cmpFn, psFn) +} diff --git a/pkg/migration/testdata/plan/configurationpkgv1.yaml b/pkg/migration/testdata/plan/configurationpkgv1.yaml new file mode 100644 index 00000000..972b8c3d --- /dev/null +++ b/pkg/migration/testdata/plan/configurationpkgv1.yaml @@ -0,0 +1,6 @@ +apiVersion: pkg.crossplane.io/v1 +kind: Configuration +metadata: + name: platform-ref-aws +spec: + package: xpkg.upbound.io/upbound/provider-ref-aws:v0.1.0 \ No newline at end of file diff --git a/pkg/migration/testdata/plan/configurationv1.yaml b/pkg/migration/testdata/plan/configurationv1.yaml new file mode 100644 index 00000000..809b0a26 --- /dev/null +++ b/pkg/migration/testdata/plan/configurationv1.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 diff --git a/pkg/migration/testdata/plan/configurationv1alpha1.yaml b/pkg/migration/testdata/plan/configurationv1alpha1.yaml new file mode 100644 index 00000000..117faf74 --- /dev/null +++ b/pkg/migration/testdata/plan/configurationv1alpha1.yaml @@ -0,0 +1,36 @@ +apiVersion: meta.pkg.crossplane.io/v1alpha1 +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 diff --git a/pkg/migration/testdata/plan/generated/activate-ssop/provider-aws-ec2.providers.pkg.crossplane.io_v1.yaml b/pkg/migration/testdata/plan/generated/activate-ssop/provider-aws-ec2.providers.pkg.crossplane.io_v1.yaml new file mode 100644 index 00000000..2d83bc3c --- /dev/null +++ b/pkg/migration/testdata/plan/generated/activate-ssop/provider-aws-ec2.providers.pkg.crossplane.io_v1.yaml @@ -0,0 +1,6 @@ +apiVersion: pkg.crossplane.io/v1 +kind: Provider +metadata: + name: provider-aws-ec2 +spec: + revisionActivationPolicy: Automatic diff --git a/pkg/migration/testdata/plan/generated/activate-ssop/provider-aws-eks.providers.pkg.crossplane.io_v1.yaml b/pkg/migration/testdata/plan/generated/activate-ssop/provider-aws-eks.providers.pkg.crossplane.io_v1.yaml new file mode 100644 index 00000000..dc9d0827 --- /dev/null +++ b/pkg/migration/testdata/plan/generated/activate-ssop/provider-aws-eks.providers.pkg.crossplane.io_v1.yaml @@ -0,0 +1,6 @@ +apiVersion: pkg.crossplane.io/v1 +kind: Provider +metadata: + name: provider-aws-eks +spec: + revisionActivationPolicy: Automatic diff --git a/pkg/migration/testdata/plan/generated/activate-ssop/provider-family-aws.providers.pkg.crossplane.io_v1.yaml b/pkg/migration/testdata/plan/generated/activate-ssop/provider-family-aws.providers.pkg.crossplane.io_v1.yaml new file mode 100644 index 00000000..8491afdc --- /dev/null +++ b/pkg/migration/testdata/plan/generated/activate-ssop/provider-family-aws.providers.pkg.crossplane.io_v1.yaml @@ -0,0 +1,6 @@ +apiVersion: pkg.crossplane.io/v1 +kind: Provider +metadata: + name: provider-family-aws +spec: + revisionActivationPolicy: Automatic diff --git a/pkg/migration/testdata/plan/generated/configurationv1_migration_plan.yaml b/pkg/migration/testdata/plan/generated/configurationv1_migration_plan.yaml new file mode 100644 index 00000000..c8905d38 --- /dev/null +++ b/pkg/migration/testdata/plan/generated/configurationv1_migration_plan.yaml @@ -0,0 +1,10 @@ +spec: + steps: + - patch: + type: merge + files: + - edit-configuration-metadata/platform-ref-aws.configurations.meta.pkg.crossplane.io_v1.yaml + name: edit-configuration-metadata + type: Patch + +version: 0.1.0 diff --git a/pkg/migration/testdata/plan/generated/configurationv1_providerv1_migration_plan.yaml b/pkg/migration/testdata/plan/generated/configurationv1_providerv1_migration_plan.yaml new file mode 100644 index 00000000..a70677c1 --- /dev/null +++ b/pkg/migration/testdata/plan/generated/configurationv1_providerv1_migration_plan.yaml @@ -0,0 +1,70 @@ +spec: + steps: + - apply: + files: + - new-ssop/provider-family-aws.providers.pkg.crossplane.io_v1.yaml + name: new-ssop + type: Apply + + - apply: + files: + - new-ssop/provider-aws-ec2.providers.pkg.crossplane.io_v1.yaml + - new-ssop/provider-aws-eks.providers.pkg.crossplane.io_v1.yaml + name: new-ssop + type: Apply + + - patch: + type: merge + files: + - disable-dependency-resolution/platform-ref-aws.configurations.pkg.crossplane.io_v1.yaml + name: disable-dependency-resolution + type: Patch + + - delete: + options: + finalizerPolicy: Remove + resources: + - group: pkg.crossplane.io + kind: Provider + name: provider-aws + version: v1 + name: delete-monolithic-provider + type: Delete + + - patch: + type: merge + files: + - activate-ssop/provider-family-aws.providers.pkg.crossplane.io_v1.yaml + name: activate-ssop + type: Patch + + - patch: + type: merge + files: + - activate-ssop/provider-aws-ec2.providers.pkg.crossplane.io_v1.yaml + - activate-ssop/provider-aws-eks.providers.pkg.crossplane.io_v1.yaml + name: activate-ssop + type: Patch + + - patch: + type: merge + files: + - edit-configuration-metadata/platform-ref-aws.configurations.meta.pkg.crossplane.io_v1.yaml + name: edit-configuration-metadata + type: Patch + + - patch: + type: merge + files: + - edit-configuration-package/platform-ref-aws.configurations.pkg.crossplane.io_v1.yaml + name: edit-configuration-package + type: Patch + + - patch: + type: merge + files: + - enable-dependency-resolution/platform-ref-aws.configurations.pkg.crossplane.io_v1.yaml + name: enable-dependency-resolution + type: Patch + +version: 0.1.0 diff --git a/pkg/migration/testdata/plan/generated/configurationv1alpha1_migration_plan.yaml b/pkg/migration/testdata/plan/generated/configurationv1alpha1_migration_plan.yaml new file mode 100644 index 00000000..3b163939 --- /dev/null +++ b/pkg/migration/testdata/plan/generated/configurationv1alpha1_migration_plan.yaml @@ -0,0 +1,10 @@ +spec: + steps: + - patch: + type: merge + files: + - edit-configuration-metadata/platform-ref-aws.configurations.meta.pkg.crossplane.io_v1alpha1.yaml + name: edit-configuration-metadata + type: Patch + +version: 0.1.0 diff --git a/pkg/migration/testdata/plan/generated/disable-dependency-resolution/platform-ref-aws.configurations.pkg.crossplane.io_v1.yaml b/pkg/migration/testdata/plan/generated/disable-dependency-resolution/platform-ref-aws.configurations.pkg.crossplane.io_v1.yaml new file mode 100644 index 00000000..5c747b09 --- /dev/null +++ b/pkg/migration/testdata/plan/generated/disable-dependency-resolution/platform-ref-aws.configurations.pkg.crossplane.io_v1.yaml @@ -0,0 +1,6 @@ +apiVersion: pkg.crossplane.io/v1 +kind: Configuration +metadata: + name: platform-ref-aws +spec: + skipDependencyResolution: true diff --git a/pkg/migration/testdata/plan/generated/edit-configuration-metadata/platform-ref-aws.configurations.meta.pkg.crossplane.io_v1.yaml b/pkg/migration/testdata/plan/generated/edit-configuration-metadata/platform-ref-aws.configurations.meta.pkg.crossplane.io_v1.yaml new file mode 100644 index 00000000..5838a30e --- /dev/null +++ b/pkg/migration/testdata/plan/generated/edit-configuration-metadata/platform-ref-aws.configurations.meta.pkg.crossplane.io_v1.yaml @@ -0,0 +1,8 @@ +apiVersion: meta.pkg.crossplane.io/v1 +kind: Configuration +metadata: + name: platform-ref-aws +spec: + dependsOn: + - provider: xpkg.upbound.io/upbound/provider-aws-eks + version: ">=v0.17.0" diff --git a/pkg/migration/testdata/plan/generated/edit-configuration-metadata/platform-ref-aws.configurations.meta.pkg.crossplane.io_v1alpha1.yaml b/pkg/migration/testdata/plan/generated/edit-configuration-metadata/platform-ref-aws.configurations.meta.pkg.crossplane.io_v1alpha1.yaml new file mode 100644 index 00000000..59430fac --- /dev/null +++ b/pkg/migration/testdata/plan/generated/edit-configuration-metadata/platform-ref-aws.configurations.meta.pkg.crossplane.io_v1alpha1.yaml @@ -0,0 +1,8 @@ +apiVersion: meta.pkg.crossplane.io/v1alpha1 +kind: Configuration +metadata: + name: platform-ref-aws +spec: + dependsOn: + - provider: xpkg.upbound.io/upbound/provider-aws-eks + version: ">=v0.17.0" diff --git a/pkg/migration/testdata/plan/generated/edit-configuration-package/platform-ref-aws.configurations.pkg.crossplane.io_v1.yaml b/pkg/migration/testdata/plan/generated/edit-configuration-package/platform-ref-aws.configurations.pkg.crossplane.io_v1.yaml new file mode 100644 index 00000000..e74b8374 --- /dev/null +++ b/pkg/migration/testdata/plan/generated/edit-configuration-package/platform-ref-aws.configurations.pkg.crossplane.io_v1.yaml @@ -0,0 +1,6 @@ +apiVersion: pkg.crossplane.io/v1 +kind: Configuration +metadata: + name: platform-ref-aws +spec: + package: xpkg.upbound.io/upbound/provider-ref-aws:v0.2.0-ssop diff --git a/pkg/migration/testdata/plan/generated/enable-dependency-resolution/platform-ref-aws.configurations.pkg.crossplane.io_v1.yaml b/pkg/migration/testdata/plan/generated/enable-dependency-resolution/platform-ref-aws.configurations.pkg.crossplane.io_v1.yaml new file mode 100644 index 00000000..d0ee5a05 --- /dev/null +++ b/pkg/migration/testdata/plan/generated/enable-dependency-resolution/platform-ref-aws.configurations.pkg.crossplane.io_v1.yaml @@ -0,0 +1,6 @@ +apiVersion: pkg.crossplane.io/v1 +kind: Configuration +metadata: + name: platform-ref-aws +spec: + skipDependencyResolution: false diff --git a/pkg/migration/testdata/plan/generated/new-ssop/provider-aws-ec2.providers.pkg.crossplane.io_v1.yaml b/pkg/migration/testdata/plan/generated/new-ssop/provider-aws-ec2.providers.pkg.crossplane.io_v1.yaml new file mode 100644 index 00000000..4f6a43dd --- /dev/null +++ b/pkg/migration/testdata/plan/generated/new-ssop/provider-aws-ec2.providers.pkg.crossplane.io_v1.yaml @@ -0,0 +1,7 @@ +apiVersion: pkg.crossplane.io/v1 +kind: Provider +metadata: + name: provider-aws-ec2 +spec: + package: xpkg.upbound.io/upbound/provider-aws-ec2:v0.37.0 + revisionActivationPolicy: Manual diff --git a/pkg/migration/testdata/plan/generated/new-ssop/provider-aws-eks.providers.pkg.crossplane.io_v1.yaml b/pkg/migration/testdata/plan/generated/new-ssop/provider-aws-eks.providers.pkg.crossplane.io_v1.yaml new file mode 100644 index 00000000..05fe4545 --- /dev/null +++ b/pkg/migration/testdata/plan/generated/new-ssop/provider-aws-eks.providers.pkg.crossplane.io_v1.yaml @@ -0,0 +1,7 @@ +apiVersion: pkg.crossplane.io/v1 +kind: Provider +metadata: + name: provider-aws-eks +spec: + package: xpkg.upbound.io/upbound/provider-aws-eks:v0.37.0 + revisionActivationPolicy: Manual diff --git a/pkg/migration/testdata/plan/generated/new-ssop/provider-family-aws.providers.pkg.crossplane.io_v1.yaml b/pkg/migration/testdata/plan/generated/new-ssop/provider-family-aws.providers.pkg.crossplane.io_v1.yaml new file mode 100644 index 00000000..eddda03d --- /dev/null +++ b/pkg/migration/testdata/plan/generated/new-ssop/provider-family-aws.providers.pkg.crossplane.io_v1.yaml @@ -0,0 +1,7 @@ +apiVersion: pkg.crossplane.io/v1 +kind: Provider +metadata: + name: provider-family-aws +spec: + package: xpkg.upbound.io/upbound/provider-family-aws:v0.37.0 + revisionActivationPolicy: Manual diff --git a/pkg/migration/testdata/plan/generated/providerv1_migration_plan.yaml b/pkg/migration/testdata/plan/generated/providerv1_migration_plan.yaml new file mode 100644 index 00000000..832195df --- /dev/null +++ b/pkg/migration/testdata/plan/generated/providerv1_migration_plan.yaml @@ -0,0 +1,42 @@ +spec: + steps: + - apply: + files: + - new-ssop/provider-family-aws.providers.pkg.crossplane.io_v1.yaml + name: new-ssop + type: Apply + + - apply: + files: + - new-ssop/provider-aws-ec2.providers.pkg.crossplane.io_v1.yaml + - new-ssop/provider-aws-eks.providers.pkg.crossplane.io_v1.yaml + name: new-ssop + type: Apply + + - delete: + options: + finalizerPolicy: Remove + resources: + - group: pkg.crossplane.io + kind: Provider + name: provider-aws + version: v1 + name: delete-monolithic-provider + type: Delete + + - patch: + type: merge + files: + - activate-ssop/provider-family-aws.providers.pkg.crossplane.io_v1.yaml + name: activate-ssop + type: Patch + + - patch: + type: merge + files: + - activate-ssop/provider-aws-ec2.providers.pkg.crossplane.io_v1.yaml + - activate-ssop/provider-aws-eks.providers.pkg.crossplane.io_v1.yaml + name: activate-ssop + type: Patch + +version: 0.1.0 diff --git a/pkg/migration/testdata/plan/providerv1.yaml b/pkg/migration/testdata/plan/providerv1.yaml new file mode 100644 index 00000000..b34a4ecd --- /dev/null +++ b/pkg/migration/testdata/plan/providerv1.yaml @@ -0,0 +1,6 @@ +apiVersion: pkg.crossplane.io/v1 +kind: Provider +metadata: + name: provider-aws +spec: + package: xpkg.upbound.io/upbound/provider-aws:v0.37.0 diff --git a/pkg/migration/types.go b/pkg/migration/types.go index c1ae72ce..5c87aaae 100644 --- a/pkg/migration/types.go +++ b/pkg/migration/types.go @@ -52,7 +52,8 @@ type Plan struct { type Spec struct { // Steps are the migration plan's steps that are expected // to complete a migration when executed in order. - Steps []Step `json:"steps,omitempty"` + Steps []Step `json:"steps,omitempty"` + stepMap map[string]*Step } // StepType is the type used to name a migration step