package rest import ( "context" "errors" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apiserver/pkg/apis/example" "k8s.io/apiserver/pkg/endpoints/request" ) var legacyObj1 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo1", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}} var legacyObj2 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo2", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}} var legacyObj3 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo3", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}} var legacyObj4 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo4", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}} var legacyObj2WithHostname = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo2", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{Hostname: "hostname"}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}} var storageObj1 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo1", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}} var storageObj2 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo2", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}} var storageObj3 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo3", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}} var storageObj4 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo4", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}} var legacyListWith3items = &example.PodList{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ListMeta: metav1.ListMeta{}, Items: []example.Pod{ *legacyObj1, *legacyObj2, *legacyObj3, }} var legacyListWith4items = &example.PodList{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ListMeta: metav1.ListMeta{}, Items: []example.Pod{ *legacyObj1, *legacyObj2, *legacyObj3, *legacyObj4, }} var legacyListWith3itemsObj2IsDifferent = &example.PodList{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ListMeta: metav1.ListMeta{}, Items: []example.Pod{ *legacyObj1, *legacyObj2WithHostname, *legacyObj3, }} var storageListWith3items = &example.PodList{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ListMeta: metav1.ListMeta{}, Items: []example.Pod{ *storageObj1, *storageObj2, *storageObj3, }} var storageListWith4items = &example.PodList{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ListMeta: metav1.ListMeta{}, Items: []example.Pod{ *storageObj1, *storageObj2, *storageObj3, *storageObj4, }} var storageListWith3itemsMissingFoo2 = &example.PodList{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ListMeta: metav1.ListMeta{}, Items: []example.Pod{ *storageObj1, *storageObj3, *storageObj4, }} func TestLegacyToUnifiedStorage_DataSyncer(t *testing.T) { type testCase struct { setupLegacyFn func(m *mock.Mock) setupStorageFn func(m *mock.Mock) name string expectedOutcome bool wantErr bool } tests := []testCase{ { name: "both stores are in sync", setupLegacyFn: func(m *mock.Mock) { m.On("List", mock.Anything, mock.Anything).Return(legacyListWith3items, nil) }, setupStorageFn: func(m *mock.Mock) { m.On("List", mock.Anything, mock.Anything).Return(storageListWith3items, nil) }, expectedOutcome: true, }, { name: "both stores are in sync - fail to list from legacy", setupLegacyFn: func(m *mock.Mock) { m.On("List", mock.Anything, mock.Anything).Return(legacyListWith3items, errors.New("error")) }, setupStorageFn: func(m *mock.Mock) { m.On("List", mock.Anything, mock.Anything).Return(storageListWith3items, nil) }, expectedOutcome: false, }, { name: "both stores are in sync - fail to list from storage", setupLegacyFn: func(m *mock.Mock) { m.On("List", mock.Anything, mock.Anything).Return(legacyListWith3items, nil) }, setupStorageFn: func(m *mock.Mock) { m.On("List", mock.Anything, mock.Anything).Return(storageListWith3items, errors.New("error")) }, expectedOutcome: false, }, { name: "storage is missing 1 entry (foo4)", setupLegacyFn: func(m *mock.Mock) { m.On("List", mock.Anything, mock.Anything).Return(legacyListWith4items, nil) }, setupStorageFn: func(m *mock.Mock) { m.On("List", mock.Anything, mock.Anything).Return(storageListWith3items, nil) m.On("Update", mock.Anything, "foo4", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, false, nil) }, expectedOutcome: true, }, { name: "storage needs to be update (foo2 is different)", setupLegacyFn: func(m *mock.Mock) { m.On("List", mock.Anything, mock.Anything).Return(legacyListWith3itemsObj2IsDifferent, nil) }, setupStorageFn: func(m *mock.Mock) { m.On("List", mock.Anything, mock.Anything).Return(storageListWith3items, nil) m.On("Update", mock.Anything, "foo2", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, false, nil) }, expectedOutcome: true, }, { name: "storage is missing 1 entry (foo4) - fail to upsert", setupLegacyFn: func(m *mock.Mock) { m.On("List", mock.Anything, mock.Anything).Return(legacyListWith4items, nil) }, setupStorageFn: func(m *mock.Mock) { m.On("List", mock.Anything, mock.Anything).Return(storageListWith3items, nil) m.On("Update", mock.Anything, "foo4", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, false, errors.New("error")) }, expectedOutcome: false, }, { name: "storage has an extra 1 entry (foo4)", setupLegacyFn: func(m *mock.Mock) { m.On("List", mock.Anything, mock.Anything).Return(legacyListWith3items, nil) }, setupStorageFn: func(m *mock.Mock) { m.On("List", mock.Anything, mock.Anything).Return(storageListWith4items, nil) m.On("Delete", mock.Anything, "foo4", mock.Anything, mock.Anything).Return(exampleObj, false, nil) }, expectedOutcome: true, }, { name: "storage has an extra 1 entry (foo4) - fail to delete", setupLegacyFn: func(m *mock.Mock) { m.On("List", mock.Anything, mock.Anything).Return(legacyListWith3items, nil) }, setupStorageFn: func(m *mock.Mock) { m.On("List", mock.Anything, mock.Anything).Return(storageListWith4items, nil) m.On("Delete", mock.Anything, "foo4", mock.Anything, mock.Anything).Return(exampleObj, false, errors.New("error")) }, expectedOutcome: false, }, { name: "storage is missing 1 entry (foo3) and has an extra 1 entry (foo4)", setupLegacyFn: func(m *mock.Mock) { m.On("List", mock.Anything, mock.Anything).Return(legacyListWith3items, nil) }, setupStorageFn: func(m *mock.Mock) { m.On("List", mock.Anything, mock.Anything).Return(storageListWith3itemsMissingFoo2, nil) m.On("Update", mock.Anything, "foo2", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, false, nil) m.On("Delete", mock.Anything, "foo4", mock.Anything, mock.Anything).Return(exampleObj, false, nil) }, expectedOutcome: true, }, } // mode 1 for _, tt := range tests { t.Run("Mode-1-"+tt.name, func(t *testing.T) { l := (Storage)(nil) s := (Storage)(nil) lm := &mock.Mock{} um := &mock.Mock{} ls := storageMock{lm, l} us := storageMock{um, s} if tt.setupLegacyFn != nil { tt.setupLegacyFn(lm) } if tt.setupStorageFn != nil { tt.setupStorageFn(um) } outcome, err := legacyToUnifiedStorageDataSyncer(context.Background(), &SyncerConfig{ Mode: Mode1, LegacyStorage: ls, Storage: us, Kind: "test.kind", Reg: p, ServerLockService: &fakeServerLock{}, RequestInfo: &request.RequestInfo{}, DataSyncerRecordsLimit: 1000, DataSyncerInterval: time.Hour, }) if tt.wantErr { assert.Error(t, err) return } assert.NoError(t, err) assert.Equal(t, tt.expectedOutcome, outcome) }) } // mode 2 for _, tt := range tests { t.Run("Mode-2-"+tt.name, func(t *testing.T) { l := (Storage)(nil) s := (Storage)(nil) lm := &mock.Mock{} um := &mock.Mock{} ls := storageMock{lm, l} us := storageMock{um, s} if tt.setupLegacyFn != nil { tt.setupLegacyFn(lm) } if tt.setupStorageFn != nil { tt.setupStorageFn(um) } outcome, err := legacyToUnifiedStorageDataSyncer(context.Background(), &SyncerConfig{ Mode: Mode2, LegacyStorage: ls, Storage: us, Kind: "test.kind", Reg: p, ServerLockService: &fakeServerLock{}, RequestInfo: &request.RequestInfo{}, DataSyncerRecordsLimit: 1000, DataSyncerInterval: time.Hour, }) if tt.wantErr { assert.Error(t, err) return } assert.NoError(t, err) assert.Equal(t, tt.expectedOutcome, outcome) }) } }