Skip to content

Commit

Permalink
Add migration tests framework and migration package tests
Browse files Browse the repository at this point in the history
Signed-off-by: Alper Rifat Ulucinar <ulucinar@users.noreply.github.com>
  • Loading branch information
ulucinar committed Dec 13, 2022
1 parent 4b0a674 commit af81b4a
Show file tree
Hide file tree
Showing 19 changed files with 1,388 additions and 11 deletions.
631 changes: 631 additions & 0 deletions pkg/migration/fake/mocks/mock.go

Large diffs are not rendered by default.

108 changes: 108 additions & 0 deletions pkg/migration/fake/objects.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright 2022 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.

//go:generate go run github.com/golang/mock/mockgen -copyright_file ../../../hack/boilerplate.txt -destination=./mocks/mock.go -package mocks github.com/crossplane/crossplane-runtime/pkg/resource Managed

package fake

import (
xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
"k8s.io/apimachinery/pkg/runtime/schema"

"github.com/upbound/upjet/pkg/migration/fake/mocks"
)

const (
MigrationSourceGroup = "fakesourceapi"
MigrationSourceVersion = "v1alpha1"
MigrationSourceKind = "VPC"

MigrationTargetGroup = "faketargetapi"
MigrationTargetVersion = "v1alpha1"
MigrationTargetKind = "VPC"
)

var (
MigrationSourceGVK = schema.GroupVersionKind{
Group: MigrationSourceGroup,
Version: MigrationSourceVersion,
Kind: MigrationSourceKind,
}

MigrationTargetGVK = schema.GroupVersionKind{
Group: MigrationTargetGroup,
Version: MigrationTargetVersion,
Kind: MigrationTargetKind,
}
)

type MigrationSourceObject struct {
mocks.MockManaged
// cannot inline v1.TypeMeta here as mocks.MockManaged is also inlined
APIVersion string `json:"apiVersion,omitempty"`
Kind string `json:"kind,omitempty"`
// cannot inline v1.ObjectMeta here as mocks.MockManaged is also inlined
ObjectMeta ObjectMeta `json:"metadata,omitempty"`
Spec SourceSpec `json:"spec"`
Status Status `json:"status,omitempty"`
}

type SourceSpec struct {
xpv1.ResourceSpec `json:",inline"`
ForProvider SourceSpecParameters `json:"forProvider"`
}

type SourceSpecParameters struct {
Region *string `json:"region,omitempty"`
CIDRBlock string `json:"cidrBlock"`
Tags []Tag `json:"tags,omitempty"`
}

type Tag struct {
Key string `json:"key"`
Value string `json:"value"`
}

type Status struct {
xpv1.ResourceStatus `json:",inline"`
AtProvider Observation `json:"atProvider,omitempty"`
}

type Observation struct{}

type MigrationTargetObject struct {
mocks.MockManaged
// cannot inline v1.TypeMeta here as mocks.MockManaged is also inlined
APIVersion string `json:"apiVersion,omitempty"`
Kind string `json:"kind,omitempty"`
// cannot inline v1.ObjectMeta here as mocks.MockManaged is also inlined
ObjectMeta ObjectMeta `json:"metadata,omitempty"`
Spec TargetSpec `json:"spec"`
Status Status `json:"status,omitempty"`
}

type ObjectMeta struct {
Name string `json:"name,omitempty"`
}

type TargetSpec struct {
xpv1.ResourceSpec `json:",inline"`
ForProvider TargetSpecParameters `json:"forProvider"`
}

type TargetSpecParameters struct {
Region *string `json:"region,omitempty"`
CIDRBlock string `json:"cidrBlock"`
Tags map[string]string `json:"tags,omitempty"`
}
13 changes: 2 additions & 11 deletions pkg/migration/plan_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -392,17 +392,8 @@ func (pg *PlanGenerator) stepDeleteOldManagedResource(u *UnstructuredWithMetadat
})
}

func addPauseAnnotation(u *unstructured.Unstructured) {
annot := u.GetAnnotations()
if annot == nil {
annot = make(map[string]string)
}
annot[meta.AnnotationKeyReconciliationPaused] = "true"
u.SetAnnotations(annot)
}

func (pg *PlanGenerator) pause(fp string, u *unstructured.Unstructured) error {
addPauseAnnotation(u)
meta.AddAnnotations(u, map[string]string{meta.AnnotationKeyReconciliationPaused: "true"})
return errors.Wrap(pg.target.Put(UnstructuredWithMetadata{
Object: *u,
Metadata: Metadata{
Expand All @@ -417,7 +408,7 @@ func getQualifiedName(u unstructured.Unstructured) string {
}

func (pg *PlanGenerator) stepNewManagedResource(u *UnstructuredWithMetadata) error {
addPauseAnnotation(&u.Object)
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 {
Expand Down
262 changes: 262 additions & 0 deletions pkg/migration/plan_generator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
// Copyright 2022 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 (
"bytes"
"fmt"
"os"
"path/filepath"
"strings"
"testing"

"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
xpresource "github.com/crossplane/crossplane-runtime/pkg/resource"
"github.com/crossplane/crossplane-runtime/pkg/test"
v1 "github.com/crossplane/crossplane/apis/apiextensions/v1"
"github.com/google/go-cmp/cmp"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/client-go/kubernetes/scheme"
k8syaml "sigs.k8s.io/yaml"

"github.com/upbound/upjet/pkg/migration/fake"
)

func TestGeneratePlan(t *testing.T) {
type fields struct {
source Source
target *testTarget
registry Registry
}
type want struct {
err error
migrationPlanPath string
// names of resource files to be loaded
migratedResourceNames []string
}
tests := map[string]struct {
fields fields
want want
}{
"PlanWithManagedResourceAndClaim": {
fields: fields{
source: newTestSource(map[string]Metadata{
"testdata/plan/sourcevpc.yaml": {},
"testdata/plan/claim.yaml": {IsClaim: true},
"testdata/plan/composition.yaml": {},
"testdata/plan/xrd.yaml": {},
"testdata/plan/xr.yaml": {IsComposite: true}}),
target: newTestTarget(),
registry: getRegistryWithConverters(map[schema.GroupVersionKind]Converter{
fake.MigrationSourceGVK: &testConverter{},
}),
},
want: want{
migrationPlanPath: "testdata/plan/migration_plan.yaml",
migratedResourceNames: []string{
"pause-managed/sample-vpc.vpcs.fakesourceapi.yaml",
"edit-claims/my-resource.myresources.test.com.yaml",
"start-managed/sample-vpc.vpcs.faketargetapi.yaml",
"pause-composites/my-resource-dwjgh.xmyresources.test.com.yaml",
"edit-composites/my-resource-dwjgh.xmyresources.test.com.yaml",
"deletion-policy-orphan/sample-vpc.vpcs.fakesourceapi.yaml",
"new-compositions/example-migrated.compositions.apiextensions.crossplane.io.yaml",
"start-composites/my-resource-dwjgh.xmyresources.test.com.yaml",
"create-new-managed/sample-vpc.vpcs.faketargetapi.yaml",
},
},
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
pg := NewPlanGenerator(tt.fields.source, tt.fields.target)
err := pg.GeneratePlan()
// compare error state
if diff := cmp.Diff(tt.want.err, err, test.EquateErrors()); diff != "" {
t.Fatalf("GeneratePlan(): -wantError, +gotError: %s", diff)
}
if err != nil {
return
}
// compare generated plan with the expected plan
p, err := loadPlan(tt.want.migrationPlanPath)
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 != "" {
t.Errorf("GeneratePlan(): -wantPlan, +gotPlan: %s", diff)
}
// compare generated migration files with the expected ones
for _, name := range tt.want.migratedResourceNames {
path := filepath.Join("testdata/plan/generated", name)
buff, err := os.ReadFile(path)
if err != nil {
t.Fatalf("Failed to read a generated migration resource from path %s: %v", path, err)
}
u := unstructured.Unstructured{}
if err := k8syaml.Unmarshal(buff, &u); err != nil {
t.Fatalf("Failed to unmarshal a generated migration resource from path %s: %v", path, err)
}
gU, ok := tt.fields.target.targetManifests[name]
if !ok {
t.Errorf("GeneratePlan(): Expected generated migration resource file not found: %s", name)
continue
}
if diff := cmp.Diff(u, gU.Object); diff != "" {
t.Errorf("GeneratePlan(): -wantMigratedResource, +gotMigratedResource with name %q: %s", name, diff)
}
delete(tt.fields.target.targetManifests, name)
}
// check for unexpected generated migration files
for name := range tt.fields.target.targetManifests {
t.Errorf("GeneratePlan(): Unexpected generated migration file: %s", name)
}
})
}
}

type testSource struct {
sourceManifests map[string]Metadata
paths []string
index int
}

func newTestSource(sourceManifests map[string]Metadata) *testSource {
result := &testSource{sourceManifests: sourceManifests}
result.paths = make([]string, 0, len(result.sourceManifests))
for k := range result.sourceManifests {
result.paths = append(result.paths, k)
}
return result
}

func (f *testSource) HasNext() (bool, error) {
return f.index <= len(f.paths)-1, nil
}

func (f *testSource) Next() (UnstructuredWithMetadata, error) {
um := UnstructuredWithMetadata{
Metadata: f.sourceManifests[f.paths[f.index]],
Object: unstructured.Unstructured{},
}
um.Metadata.Path = f.paths[f.index]
buff, err := os.ReadFile(f.paths[f.index])
if err != nil {
return um, err
}
decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewBufferString(string(buff)), 1024)
if err := decoder.Decode(&um.Object); err != nil {
return um, err
}
f.index++
return um, nil
}

type testTarget struct {
targetManifests map[string]UnstructuredWithMetadata
}

func newTestTarget() *testTarget {
return &testTarget{
targetManifests: make(map[string]UnstructuredWithMetadata),
}
}

func (f *testTarget) Put(o UnstructuredWithMetadata) error {
f.targetManifests[o.Metadata.Path] = o
return nil
}

func (f *testTarget) Delete(o UnstructuredWithMetadata) error {
delete(f.targetManifests, o.Metadata.Path)
return nil
}

// can be utilized to populate test artifacts
/*func (f *testTarget) dumpFiles(parentDir string) error {
for f, u := range f.targetManifests {
path := filepath.Join(parentDir, f)
buff, err := k8syaml.Marshal(u.Object.Object)
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
if err := os.WriteFile(path, buff, 0o600); err != nil {
return err
}
}
return nil
}*/

type testConverter struct{}

func (f *testConverter) Resources(mg xpresource.Managed) ([]xpresource.Managed, error) {
s := mg.(*fake.MigrationSourceObject)
t := &fake.MigrationTargetObject{}
if _, err := CopyInto(s, t, fake.MigrationTargetGVK, "spec.forProvider.tags", "mockManaged"); err != nil {
return nil, err
}
t.Spec.ForProvider.Tags = make(map[string]string, len(s.Spec.ForProvider.Tags))
for _, tag := range s.Spec.ForProvider.Tags {
v := tag.Value
t.Spec.ForProvider.Tags[tag.Key] = v
}
return []xpresource.Managed{
t,
}, nil
}

func (f *testConverter) ComposedTemplates(cmp v1.ComposedTemplate, convertedBase ...*v1.ComposedTemplate) error {
for i, cb := range convertedBase {
for j, p := range cb.Patches {
if p.ToFieldPath == nil || !strings.HasPrefix(*p.ToFieldPath, "spec.forProvider.tags") {
continue
}
u, err := FromRawExtension(cmp.Base)
if err != nil {
return err
}
paved := fieldpath.Pave(u.Object)
key, err := paved.GetString(strings.ReplaceAll(*p.ToFieldPath, ".value", ".key"))
if err != nil {
return err
}
s := fmt.Sprintf(`spec.forProvider.tags["%s"]`, key)
convertedBase[i].Patches[j].ToFieldPath = &s
}
}
return nil
}

func getRegistryWithConverters(converters map[schema.GroupVersionKind]Converter) Registry {
scheme.Scheme.AddKnownTypeWithName(fake.MigrationSourceGVK, &fake.MigrationSourceObject{})
for gvk, c := range converters {
RegisterConverter(gvk, c)
}
return registry
}

func loadPlan(planPath string) (*Plan, error) {
buff, err := os.ReadFile(planPath)
if err != nil {
return nil, err
}
p := &Plan{}
return p, k8syaml.Unmarshal(buff, p)
}
Loading

0 comments on commit af81b4a

Please sign in to comment.