911 lines
22 KiB
Go
911 lines
22 KiB
Go
package kubernetes
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/open-feature/flagd/pkg/logger"
|
|
"github.com/open-feature/flagd/pkg/sync"
|
|
"github.com/open-feature/open-feature-operator/apis/core/v1alpha1"
|
|
"go.uber.org/zap/zapcore"
|
|
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/client-go/dynamic/fake"
|
|
"k8s.io/client-go/kubernetes/scheme"
|
|
"k8s.io/client-go/tools/cache"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
fakeClient "sigs.k8s.io/controller-runtime/pkg/client/fake"
|
|
"sigs.k8s.io/controller-runtime/pkg/controller/controllertest"
|
|
)
|
|
|
|
var Metadata = v1.TypeMeta{
|
|
Kind: "FeatureFlagConfiguration",
|
|
APIVersion: apiVersion,
|
|
}
|
|
|
|
func Test_parseURI(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
uri string
|
|
ns string
|
|
resource string
|
|
err bool
|
|
}{
|
|
{
|
|
name: "simple success",
|
|
uri: "namespace/resource",
|
|
ns: "namespace",
|
|
resource: "resource",
|
|
err: false,
|
|
},
|
|
{
|
|
name: "simple error - no ns",
|
|
uri: "/resource",
|
|
err: true,
|
|
},
|
|
{
|
|
name: "simple error - no resource",
|
|
uri: "resource/",
|
|
err: true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ns, rs, err := parseURI(tt.uri)
|
|
if (err != nil) != tt.err {
|
|
t.Errorf("parseURI() error = %v, wantErr %v", err, tt.err)
|
|
return
|
|
}
|
|
if ns != tt.ns {
|
|
t.Errorf("parseURI() got = %v, want %v", ns, tt.ns)
|
|
}
|
|
if rs != tt.resource {
|
|
t.Errorf("parseURI() got1 = %v, want %v", rs, tt.resource)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_toFFCfg(t *testing.T) {
|
|
validFFCfg := v1alpha1.FeatureFlagConfiguration{
|
|
TypeMeta: Metadata,
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
input interface{}
|
|
want *v1alpha1.FeatureFlagConfiguration
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "Simple success",
|
|
input: toUnstructured(t, validFFCfg),
|
|
want: &validFFCfg,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "Simple error",
|
|
input: struct {
|
|
flag string
|
|
}{
|
|
flag: "test",
|
|
},
|
|
wantErr: true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := toFFCfg(tt.input)
|
|
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("toFFCfg() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
|
|
if !reflect.DeepEqual(got, tt.want) {
|
|
t.Errorf("toFFCfg() got = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_commonHandler(t *testing.T) {
|
|
cfgNs := "resourceNS"
|
|
cfgName := "resourceName"
|
|
|
|
validFFCfg := v1alpha1.FeatureFlagConfiguration{
|
|
TypeMeta: Metadata,
|
|
ObjectMeta: v1.ObjectMeta{
|
|
Namespace: cfgNs,
|
|
Name: cfgName,
|
|
},
|
|
}
|
|
|
|
type args struct {
|
|
obj interface{}
|
|
object client.ObjectKey
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
wantErr bool
|
|
wantEvent bool
|
|
eventType DefaultEventType
|
|
}{
|
|
{
|
|
name: "simple success",
|
|
args: args{
|
|
obj: toUnstructured(t, validFFCfg),
|
|
object: client.ObjectKey{
|
|
Namespace: cfgNs,
|
|
Name: cfgName,
|
|
},
|
|
},
|
|
wantEvent: true,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "simple scenario - only notify if resource name matches",
|
|
args: args{
|
|
obj: toUnstructured(t, validFFCfg),
|
|
object: client.ObjectKey{
|
|
Namespace: cfgNs,
|
|
Name: "SomeOtherResource",
|
|
},
|
|
},
|
|
wantEvent: false,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "simple error - API mismatch",
|
|
args: args{
|
|
obj: toUnstructured(t, v1alpha1.FeatureFlagConfiguration{
|
|
TypeMeta: v1.TypeMeta{
|
|
Kind: "FeatureFlagConfiguration",
|
|
APIVersion: "someAPIVersion",
|
|
},
|
|
}),
|
|
object: client.ObjectKey{
|
|
Namespace: cfgNs,
|
|
Name: cfgName,
|
|
},
|
|
},
|
|
wantErr: true,
|
|
wantEvent: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
syncChan := make(chan INotify, 1)
|
|
|
|
err := commonHandler(tt.args.obj, tt.args.object, tt.eventType, syncChan)
|
|
if err != nil && !tt.wantErr {
|
|
t.Errorf("commonHandler() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Errorf("commonHandler() expected error but received none.")
|
|
}
|
|
|
|
// Expected error occurred, hence continue
|
|
return
|
|
}
|
|
|
|
if tt.wantEvent != true {
|
|
// Not interested in the event, hence ignore notification check. But check for chan writes
|
|
if len(syncChan) != 0 {
|
|
t.Errorf("commonHandler() expected no events, but events are available: %d", len(syncChan))
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// watch events with a timeout
|
|
var notify INotify
|
|
select {
|
|
case notify = <-syncChan:
|
|
case <-time.After(2 * time.Second):
|
|
t.Errorf("timedout waiting for events from commonHandler()")
|
|
}
|
|
|
|
if notify.GetEvent().EventType != tt.eventType {
|
|
t.Errorf("commonHandler() event = %v, wanted %v", notify.GetEvent().EventType, DefaultEventTypeDelete)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_updateFuncHandler(t *testing.T) {
|
|
cfgNs := "resourceNS"
|
|
cfgName := "resourceName"
|
|
|
|
validFFCfgOld := v1alpha1.FeatureFlagConfiguration{
|
|
TypeMeta: Metadata,
|
|
ObjectMeta: v1.ObjectMeta{
|
|
Namespace: cfgNs,
|
|
Name: cfgName,
|
|
ResourceVersion: "v1",
|
|
},
|
|
}
|
|
|
|
validFFCfgNew := validFFCfgOld
|
|
validFFCfgNew.ResourceVersion = "v2"
|
|
|
|
type args struct {
|
|
oldObj interface{}
|
|
newObj interface{}
|
|
object client.ObjectKey
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
wantErr bool
|
|
wantEvent bool
|
|
}{
|
|
{
|
|
name: "Simple success",
|
|
args: args{
|
|
oldObj: toUnstructured(t, validFFCfgOld),
|
|
newObj: toUnstructured(t, validFFCfgNew),
|
|
object: client.ObjectKey{
|
|
Namespace: cfgNs,
|
|
Name: cfgName,
|
|
},
|
|
},
|
|
wantErr: false,
|
|
wantEvent: true,
|
|
},
|
|
{
|
|
name: "Simple scenario - notify only if resource name match",
|
|
args: args{
|
|
oldObj: toUnstructured(t, validFFCfgOld),
|
|
newObj: toUnstructured(t, validFFCfgNew),
|
|
object: client.ObjectKey{
|
|
Namespace: cfgNs,
|
|
Name: "SomeOtherResource",
|
|
},
|
|
},
|
|
wantErr: false,
|
|
wantEvent: false,
|
|
},
|
|
{
|
|
name: "Simple scenario - notify only if resource version is new",
|
|
args: args{
|
|
oldObj: toUnstructured(t, validFFCfgOld),
|
|
newObj: toUnstructured(t, validFFCfgOld),
|
|
object: client.ObjectKey{
|
|
Namespace: cfgNs,
|
|
Name: "SomeOtherResource",
|
|
},
|
|
},
|
|
wantErr: false,
|
|
wantEvent: false,
|
|
},
|
|
{
|
|
name: "Simple error - API version mismatch new object",
|
|
args: args{
|
|
oldObj: toUnstructured(t, validFFCfgOld),
|
|
newObj: toUnstructured(t, v1alpha1.FeatureFlagConfiguration{
|
|
TypeMeta: v1.TypeMeta{
|
|
Kind: "FeatureFlagConfiguration",
|
|
APIVersion: "someAPIVersion",
|
|
},
|
|
}),
|
|
object: client.ObjectKey{
|
|
Namespace: cfgNs,
|
|
Name: cfgName,
|
|
},
|
|
},
|
|
wantErr: true,
|
|
wantEvent: false,
|
|
},
|
|
{
|
|
name: "Simple error - API version mismatch old object",
|
|
args: args{
|
|
oldObj: toUnstructured(t, v1alpha1.FeatureFlagConfiguration{
|
|
TypeMeta: v1.TypeMeta{
|
|
Kind: "FeatureFlagConfiguration",
|
|
APIVersion: "someAPIVersion",
|
|
},
|
|
}),
|
|
newObj: toUnstructured(t, validFFCfgNew),
|
|
object: client.ObjectKey{
|
|
Namespace: cfgNs,
|
|
Name: cfgName,
|
|
},
|
|
},
|
|
wantErr: true,
|
|
wantEvent: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
syncChan := make(chan INotify, 1)
|
|
|
|
err := updateFuncHandler(tt.args.oldObj, tt.args.newObj, tt.args.object, syncChan)
|
|
if err != nil && !tt.wantErr {
|
|
t.Errorf("updateFuncHandler() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Errorf("updateFuncHandler() expected error but received none.")
|
|
}
|
|
|
|
// Expected error occurred, hence continue
|
|
return
|
|
}
|
|
|
|
if tt.wantEvent != true {
|
|
// Not interested in the event, hence ignore notification check. But check for chan writes
|
|
if len(syncChan) != 0 {
|
|
t.Errorf("updateFuncHandler() expected no events, but events are available: %d", len(syncChan))
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// watch events with a timeout
|
|
var notify INotify
|
|
select {
|
|
case notify = <-syncChan:
|
|
case <-time.After(2 * time.Second):
|
|
t.Errorf("timedout waiting for events from updateFuncHandler()")
|
|
}
|
|
|
|
if notify.GetEvent().EventType != DefaultEventTypeModify {
|
|
t.Errorf("updateFuncHandler() event = %v, wanted %v", notify.GetEvent().EventType, DefaultEventTypeModify)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSync_fetch(t *testing.T) {
|
|
flagSpec := "fakeFlagSpec"
|
|
|
|
validCfg := v1alpha1.FeatureFlagConfiguration{
|
|
TypeMeta: Metadata,
|
|
ObjectMeta: v1.ObjectMeta{
|
|
Namespace: "resourceNS",
|
|
Name: "resourceName",
|
|
ResourceVersion: "v1",
|
|
},
|
|
Spec: v1alpha1.FeatureFlagConfigurationSpec{
|
|
FeatureFlagSpec: flagSpec,
|
|
},
|
|
}
|
|
|
|
type args struct {
|
|
InformerGetFunc func(key string) (item interface{}, exists bool, err error)
|
|
ClientResponse v1alpha1.FeatureFlagConfiguration
|
|
ClientError error
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
want string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "Scenario - get from informer cache",
|
|
args: args{
|
|
InformerGetFunc: func(key string) (item interface{}, exists bool, err error) {
|
|
return toUnstructured(t, validCfg), true, nil
|
|
},
|
|
},
|
|
wantErr: false,
|
|
want: flagSpec,
|
|
},
|
|
{
|
|
name: "Scenario - get from API if informer cache miss",
|
|
args: args{
|
|
InformerGetFunc: func(key string) (item interface{}, exists bool, err error) {
|
|
return nil, false, nil
|
|
},
|
|
ClientResponse: validCfg,
|
|
},
|
|
wantErr: false,
|
|
want: flagSpec,
|
|
},
|
|
{
|
|
name: "Scenario - error for informer cache read error",
|
|
args: args{
|
|
InformerGetFunc: func(key string) (item interface{}, exists bool, err error) {
|
|
return nil, false, errors.New("mock error")
|
|
},
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "Scenario - error for API get error",
|
|
args: args{
|
|
InformerGetFunc: func(key string) (item interface{}, exists bool, err error) {
|
|
return nil, false, nil
|
|
},
|
|
ClientError: errors.New("mock error"),
|
|
},
|
|
wantErr: true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Setup with args
|
|
k := &Sync{
|
|
informer: &MockInformer{
|
|
fakeStore: cache.FakeCustomStore{
|
|
GetByKeyFunc: tt.args.InformerGetFunc,
|
|
},
|
|
},
|
|
readClient: &MockClient{
|
|
getResponse: tt.args.ClientResponse,
|
|
clientErr: tt.args.ClientError,
|
|
},
|
|
logger: logger.NewLogger(nil, false),
|
|
}
|
|
|
|
// Test fetch
|
|
got, err := k.fetch(context.Background())
|
|
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("fetch() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
|
|
if got != tt.want {
|
|
t.Errorf("fetch() got = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSync_watcher(t *testing.T) {
|
|
flagSpec := "fakeFlagSpec"
|
|
|
|
validCfg := v1alpha1.FeatureFlagConfiguration{
|
|
TypeMeta: Metadata,
|
|
ObjectMeta: v1.ObjectMeta{
|
|
Namespace: "resourceNS",
|
|
Name: "resourceName",
|
|
ResourceVersion: "v1",
|
|
},
|
|
Spec: v1alpha1.FeatureFlagConfigurationSpec{
|
|
FeatureFlagSpec: flagSpec,
|
|
},
|
|
}
|
|
|
|
type args struct {
|
|
InformerGetFunc func(key string) (item interface{}, exists bool, err error)
|
|
notification INotify
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
want string
|
|
}{
|
|
{
|
|
name: "scenario - create event",
|
|
want: flagSpec,
|
|
args: args{
|
|
InformerGetFunc: func(key string) (item interface{}, exists bool, err error) {
|
|
return toUnstructured(t, validCfg), true, nil
|
|
},
|
|
notification: &Notifier{
|
|
Event: Event[DefaultEventType]{
|
|
EventType: DefaultEventTypeCreate,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "scenario - modify event",
|
|
want: flagSpec,
|
|
args: args{
|
|
InformerGetFunc: func(key string) (item interface{}, exists bool, err error) {
|
|
return toUnstructured(t, validCfg), true, nil
|
|
},
|
|
notification: &Notifier{
|
|
Event: Event[DefaultEventType]{
|
|
EventType: DefaultEventTypeModify,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// setup sync
|
|
k := &Sync{
|
|
informer: &MockInformer{
|
|
fakeStore: cache.FakeCustomStore{
|
|
GetByKeyFunc: tt.args.InformerGetFunc,
|
|
},
|
|
},
|
|
logger: logger.NewLogger(nil, false),
|
|
}
|
|
|
|
// create communication channels with buffer to so that calls are non-blocking
|
|
notifies := make(chan INotify, 1)
|
|
dataSyncs := make(chan sync.DataSync, 1)
|
|
|
|
// emit event
|
|
notifies <- tt.args.notification
|
|
|
|
tCtx, cFunc := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cFunc()
|
|
|
|
// start watcher
|
|
go k.watcher(tCtx, notifies, dataSyncs)
|
|
|
|
// wait for data sync
|
|
select {
|
|
case <-tCtx.Done():
|
|
t.Errorf("timeout waiting for the results")
|
|
case dataSyncs := <-dataSyncs:
|
|
if dataSyncs.FlagData != tt.want {
|
|
t.Errorf("fetch() got = %v, want %v", dataSyncs.FlagData, tt.want)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInit(t *testing.T) {
|
|
t.Run("expect error with wrong URI format", func(t *testing.T) {
|
|
k := Sync{URI: ""}
|
|
e := k.Init(context.TODO())
|
|
if e == nil {
|
|
t.Errorf("Expected error but got none")
|
|
}
|
|
if k.IsReady() {
|
|
t.Errorf("Expected NOT to be ready")
|
|
}
|
|
})
|
|
t.Run("expect informer registration", func(t *testing.T) {
|
|
const name = "myFF"
|
|
const ns = "myNS"
|
|
scheme := runtime.NewScheme()
|
|
ff := &unstructured.Unstructured{}
|
|
ff.SetUnstructuredContent(getCFG(name, ns))
|
|
fakeClient := fake.NewSimpleDynamicClient(scheme, ff)
|
|
k := Sync{
|
|
URI: fmt.Sprintf("%s/%s", ns, name),
|
|
dynamicClient: fakeClient,
|
|
namespace: ns,
|
|
}
|
|
e := k.Init(context.TODO())
|
|
if e != nil {
|
|
t.Errorf("Unexpected error: %v", e)
|
|
}
|
|
if k.informer == nil {
|
|
t.Errorf("Informer not initialized")
|
|
}
|
|
if k.IsReady() {
|
|
t.Errorf("The Sync should not be ready")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSync_ReSync(t *testing.T) {
|
|
const name = "myFF"
|
|
const ns = "myNS"
|
|
s := runtime.NewScheme()
|
|
ff := &unstructured.Unstructured{}
|
|
ff.SetUnstructuredContent(getCFG(name, ns))
|
|
fakeDynamicClient := fake.NewSimpleDynamicClient(s, ff)
|
|
validFFCfg := &v1alpha1.FeatureFlagConfiguration{
|
|
TypeMeta: Metadata,
|
|
ObjectMeta: v1.ObjectMeta{
|
|
Name: name,
|
|
Namespace: ns,
|
|
},
|
|
}
|
|
fakeReadClient := newFakeReadClient(validFFCfg)
|
|
l, err := logger.NewZapLogger(zapcore.FatalLevel, "console")
|
|
if err != nil {
|
|
t.Errorf("Unexpected error: %v", err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
k Sync
|
|
countMsg int
|
|
async bool
|
|
}{
|
|
{
|
|
name: "Happy Path",
|
|
k: Sync{
|
|
URI: fmt.Sprintf("%s/%s", ns, name),
|
|
dynamicClient: fakeDynamicClient,
|
|
readClient: fakeReadClient,
|
|
namespace: ns,
|
|
logger: logger.NewLogger(l, true),
|
|
},
|
|
countMsg: 2, // one for sync and one for resync
|
|
async: true,
|
|
},
|
|
{
|
|
name: "CRD not found",
|
|
k: Sync{
|
|
URI: fmt.Sprintf("doesnt%s/exist%s", ns, name),
|
|
dynamicClient: fakeDynamicClient,
|
|
readClient: fakeReadClient,
|
|
namespace: ns,
|
|
logger: logger.NewLogger(l, true),
|
|
},
|
|
countMsg: 0,
|
|
async: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
e := tt.k.Init(context.TODO())
|
|
if e != nil {
|
|
t.Errorf("Unexpected error: %v", e)
|
|
}
|
|
if tt.k.IsReady() {
|
|
t.Errorf("The Sync should not be ready")
|
|
}
|
|
dataChannel := make(chan sync.DataSync, tt.countMsg)
|
|
if tt.async {
|
|
go func() {
|
|
if err := tt.k.Sync(context.TODO(), dataChannel); err != nil {
|
|
t.Errorf("Unexpected error: %v", e)
|
|
}
|
|
if err := tt.k.ReSync(context.TODO(), dataChannel); err != nil {
|
|
t.Errorf("Unexpected error: %v", e)
|
|
}
|
|
}()
|
|
i := tt.countMsg
|
|
for i > 0 {
|
|
d := <-dataChannel
|
|
if d.Type != sync.ALL {
|
|
t.Errorf("Expected %v, got %v", sync.ALL, d)
|
|
}
|
|
i--
|
|
}
|
|
} else {
|
|
if err := tt.k.Sync(context.TODO(), dataChannel); !strings.Contains(err.Error(), "not found") {
|
|
t.Errorf("Unexpected error: %v", err)
|
|
}
|
|
if err := tt.k.ReSync(context.TODO(), dataChannel); !strings.Contains(err.Error(), "not found") {
|
|
t.Errorf("Unexpected error: %v", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNotify(t *testing.T) {
|
|
const name = "myFF"
|
|
const ns = "myNS"
|
|
s := runtime.NewScheme()
|
|
ff := &unstructured.Unstructured{}
|
|
cfg := getCFG(name, ns)
|
|
ff.SetUnstructuredContent(cfg)
|
|
fc := fake.NewSimpleDynamicClient(s, ff)
|
|
l, err := logger.NewZapLogger(zapcore.FatalLevel, "console")
|
|
if err != nil {
|
|
t.Errorf("Unexpected error: %v", err)
|
|
}
|
|
k := Sync{
|
|
URI: fmt.Sprintf("%s/%s", ns, name),
|
|
dynamicClient: fc,
|
|
namespace: ns,
|
|
logger: logger.NewLogger(l, true),
|
|
}
|
|
err = k.Init(context.TODO())
|
|
if err != nil {
|
|
t.Errorf("Unexpected error: %v", err)
|
|
}
|
|
if k.informer == nil {
|
|
t.Errorf("Informer not initialized")
|
|
}
|
|
c := make(chan INotify)
|
|
go func() { k.notify(context.TODO(), c) }()
|
|
|
|
if k.IsReady() {
|
|
t.Errorf("The Sync should not be ready")
|
|
}
|
|
|
|
// wait for informer callbacks to be set
|
|
msg := <-c
|
|
if msg.GetEvent().EventType != DefaultEventTypeReady {
|
|
t.Errorf("Expected message %v, got %v", DefaultEventTypeReady, msg)
|
|
}
|
|
// create
|
|
cfg["status"] = map[string]interface{}{
|
|
"empty": "",
|
|
}
|
|
ff.SetUnstructuredContent(cfg)
|
|
_, err = fc.Resource(featureFlagConfigurationResource).Namespace(ns).UpdateStatus(context.TODO(), ff, v1.UpdateOptions{})
|
|
if err != nil {
|
|
t.Errorf("Unexpected error: %v", err)
|
|
}
|
|
msg = <-c
|
|
if msg.GetEvent().EventType != DefaultEventTypeCreate {
|
|
t.Errorf("Expected message %v, got %v", DefaultEventTypeCreate, msg)
|
|
}
|
|
// update
|
|
old := cfg["metadata"].(map[string]interface{})
|
|
old["resourceVersion"] = "newVersion"
|
|
cfg["metadata"] = old
|
|
ff.SetUnstructuredContent(cfg)
|
|
_, err = fc.Resource(featureFlagConfigurationResource).Namespace(ns).UpdateStatus(context.TODO(), ff, v1.UpdateOptions{})
|
|
if err != nil {
|
|
t.Errorf("Unexpected error: %v", err)
|
|
}
|
|
msg = <-c
|
|
if msg.GetEvent().EventType != DefaultEventTypeModify {
|
|
t.Errorf("Expected message %v, got %v", DefaultEventTypeModify, msg)
|
|
}
|
|
// delete
|
|
err = fc.Resource(featureFlagConfigurationResource).Namespace(ns).Delete(context.TODO(), name, v1.DeleteOptions{})
|
|
if err != nil {
|
|
t.Errorf("Unexpected error: %v", err)
|
|
}
|
|
msg = <-c
|
|
if msg.GetEvent().EventType != DefaultEventTypeDelete {
|
|
t.Errorf("Expected message %v, got %v", DefaultEventTypeDelete, msg)
|
|
}
|
|
|
|
// validate we don't crash parsing wrong spec
|
|
cfg["spec"] = map[string]interface{}{
|
|
"featureFlagSpec": int64(12), // we expect string here
|
|
}
|
|
ff.SetUnstructuredContent(cfg)
|
|
_, err = fc.Resource(featureFlagConfigurationResource).Namespace(ns).Create(context.TODO(), ff, v1.CreateOptions{})
|
|
if err != nil {
|
|
t.Errorf("Unexpected error: %v", err)
|
|
}
|
|
cfg["status"] = map[string]interface{}{
|
|
"bump": "1",
|
|
}
|
|
ff.SetUnstructuredContent(cfg)
|
|
_, err = fc.Resource(featureFlagConfigurationResource).Namespace(ns).UpdateStatus(context.TODO(), ff, v1.UpdateOptions{})
|
|
if err != nil {
|
|
t.Errorf("Unexpected error: %v", err)
|
|
}
|
|
err = fc.Resource(featureFlagConfigurationResource).Namespace(ns).Delete(context.TODO(), name, v1.DeleteOptions{})
|
|
if err != nil {
|
|
t.Errorf("Unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func Test_k8sClusterConfig(t *testing.T) {
|
|
t.Run("Cannot find KUBECONFIG file", func(tt *testing.T) {
|
|
tt.Setenv("KUBECONFIG", "")
|
|
_, err := k8sClusterConfig()
|
|
if err == nil {
|
|
tt.Error("Expected error but got none")
|
|
}
|
|
})
|
|
t.Run("KUBECONFIG file not existing", func(tt *testing.T) {
|
|
tt.Setenv("KUBECONFIG", "value")
|
|
_, err := k8sClusterConfig()
|
|
if err == nil {
|
|
tt.Error("Expected error but got none")
|
|
}
|
|
})
|
|
t.Run("Default REST Config and missing svc account", func(tt *testing.T) {
|
|
tt.Setenv("KUBERNETES_SERVICE_HOST", "127.0.0.1")
|
|
tt.Setenv("KUBERNETES_SERVICE_PORT", "8080")
|
|
_, err := k8sClusterConfig()
|
|
if err == nil {
|
|
tt.Error("Expected error but got none")
|
|
}
|
|
})
|
|
}
|
|
|
|
func Test_NewK8sSync(t *testing.T) {
|
|
l, err := logger.NewZapLogger(zapcore.FatalLevel, "console")
|
|
if err != nil {
|
|
t.Errorf("Unexpected error: %v", err)
|
|
}
|
|
const uri = "myURI"
|
|
log := logger.NewLogger(l, true)
|
|
rc := newFakeReadClient()
|
|
dc := fake.NewSimpleDynamicClient(runtime.NewScheme())
|
|
k := NewK8sSync(
|
|
log,
|
|
uri,
|
|
rc,
|
|
dc,
|
|
)
|
|
if k == nil {
|
|
t.Errorf("Object not initialized properly")
|
|
}
|
|
if k.URI != uri {
|
|
t.Errorf("Object not initialized with the right URI")
|
|
}
|
|
if k.logger != log {
|
|
t.Errorf("Object not initialized with the right logger")
|
|
}
|
|
if k.readClient != rc {
|
|
t.Errorf("Object not initialized with the right K8s client")
|
|
}
|
|
if k.dynamicClient != dc {
|
|
t.Errorf("Object not initialized with the right K8s dynamic client")
|
|
}
|
|
}
|
|
|
|
func newFakeReadClient(objs ...client.Object) client.Client {
|
|
_ = v1alpha1.AddToScheme(scheme.Scheme)
|
|
return fakeClient.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(objs...).Build()
|
|
}
|
|
|
|
func getCFG(name, namespace string) map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"apiVersion": "core.openfeature.dev/v1alpha1",
|
|
"kind": "FeatureFlagConfiguration",
|
|
"metadata": map[string]interface{}{
|
|
"name": name,
|
|
"namespace": namespace,
|
|
},
|
|
"spec": map[string]interface{}{},
|
|
}
|
|
}
|
|
|
|
// toUnstructured helper to convert an interface to unstructured.Unstructured
|
|
func toUnstructured(t *testing.T, obj interface{}) interface{} {
|
|
bytes, err := json.Marshal(obj)
|
|
if err != nil {
|
|
t.Errorf("test setup faulure: %s", err.Error())
|
|
}
|
|
|
|
var res map[string]interface{}
|
|
|
|
err = json.Unmarshal(bytes, &res)
|
|
if err != nil {
|
|
t.Errorf("test setup faulure: %s", err.Error())
|
|
}
|
|
|
|
return &unstructured.Unstructured{Object: res}
|
|
}
|
|
|
|
// Mock implementations
|
|
|
|
// MockClient contains an embedded client.Reader for desired method overriding
|
|
type MockClient struct {
|
|
client.Reader
|
|
clientErr error
|
|
|
|
getResponse v1alpha1.FeatureFlagConfiguration
|
|
}
|
|
|
|
func (m MockClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
|
|
// return error if error is set
|
|
if m.clientErr != nil {
|
|
return m.clientErr
|
|
}
|
|
|
|
// else try returning response
|
|
cfg, ok := obj.(*v1alpha1.FeatureFlagConfiguration)
|
|
if !ok {
|
|
return errors.New("must contain a pointer typed v1alpha1.FeatureFlagConfiguration")
|
|
}
|
|
|
|
*cfg = m.getResponse
|
|
return nil
|
|
}
|
|
|
|
// MockInformer contains an embedded controllertest.FakeInformer for desired method overriding
|
|
type MockInformer struct {
|
|
controllertest.FakeInformer
|
|
|
|
fakeStore cache.FakeCustomStore
|
|
}
|
|
|
|
func (m MockInformer) GetStore() cache.Store {
|
|
return &m.fakeStore
|
|
}
|