-
Notifications
You must be signed in to change notification settings - Fork 91
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add migration tests framework and migration package tests
Signed-off-by: Alper Rifat Ulucinar <ulucinar@users.noreply.github.com>
- Loading branch information
Showing
19 changed files
with
1,388 additions
and
11 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
Oops, something went wrong.