Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package csiconfigobservercontroller

import (
"context"
"testing"

"k8s.io/apimachinery/pkg/api/equality"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"

"github.com/google/go-cmp/cmp"
configv1 "github.com/openshift/api/config/v1"

opv1 "github.com/openshift/api/operator/v1"
fakeconfig "github.com/openshift/client-go/config/clientset/versioned/fake"
configinformers "github.com/openshift/client-go/config/informers/externalversions"

"github.com/openshift/library-go/pkg/controller/factory"
"github.com/openshift/library-go/pkg/operator/events"
"github.com/openshift/library-go/pkg/operator/v1helpers"
)

const (
controllerName = "TestCSIDriverControllerServiceController"
operandName = "test-csi-driver"
defaultHTTPProxyValue = "http://foo.bar.proxy"
alternativeHTTPProxyValue = "http://foo.bar.proxy.alternative"
noHTTPProxyValue = ""
)

type testCase struct {
name string
initialObjects testObjects
expectedObjects testObjects
expectErr bool
}

type testObjects struct {
proxy *configv1.Proxy
driver *fakeDriverInstance
}

type testContext struct {
controller *CSIConfigObserverController
operatorClient v1helpers.OperatorClient
}

func newTestContext(test testCase, t *testing.T) *testContext {
// Add the fake proxy to the informer
configClient := fakeconfig.NewSimpleClientset(test.initialObjects.proxy)
configInformerFactory := configinformers.NewSharedInformerFactory(configClient, 0)
configInformerFactory.Config().V1().Proxies().Informer().GetIndexer().Add(test.initialObjects.proxy)

// fakeDriverInstance also fulfils the OperatorClient interface
fakeOperatorClient := v1helpers.NewFakeOperatorClient(
&test.initialObjects.driver.Spec,
&test.initialObjects.driver.Status,
nil, /*triggerErr func*/
)

controller := NewCSIConfigObserverController(
controllerName,
fakeOperatorClient,
configInformerFactory,
events.NewInMemoryRecorder(operandName),
)

return &testContext{
controller: controller,
operatorClient: fakeOperatorClient,
}
}

// Drivers

type driverModifier func(*fakeDriverInstance) *fakeDriverInstance

func makeFakeDriverInstance(modifiers ...driverModifier) *fakeDriverInstance {
instance := &fakeDriverInstance{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster",
Generation: 0,
},
Spec: opv1.OperatorSpec{
ManagementState: opv1.Managed,
},
Status: opv1.OperatorStatus{},
}
for _, modifier := range modifiers {
instance = modifier(instance)
}
return instance
}

func withHTTPProxy(proxy string) driverModifier {
return func(i *fakeDriverInstance) *fakeDriverInstance {
observedConfig := map[string]interface{}{}
unstructured.SetNestedStringMap(observedConfig, map[string]string{"HTTP_PROXY": proxy}, ProxyConfigPath()...)

i.Spec.ObservedConfig = runtime.RawExtension{Object: &unstructured.Unstructured{Object: observedConfig}}
return i
}
}

// Proxy

func makeFakeProxyInstance(proxy string) *configv1.Proxy {
instance := &configv1.Proxy{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster",
Generation: 0,
},
Spec: configv1.ProxySpec{},
Status: configv1.ProxyStatus{},
}
if proxy != "" {
instance.Spec = configv1.ProxySpec{HTTPProxy: proxy}
instance.Status = configv1.ProxyStatus{HTTPProxy: proxy}
}
return instance

}

func TestSync(t *testing.T) {
testCases := []testCase{
{
name: "proxy exists: config is observed",
initialObjects: testObjects{
proxy: makeFakeProxyInstance(defaultHTTPProxyValue),
driver: makeFakeDriverInstance(),
},
expectedObjects: testObjects{
driver: makeFakeDriverInstance(withHTTPProxy(defaultHTTPProxyValue)),
},
},
{
name: "no proxy: config is observed",
initialObjects: testObjects{
proxy: makeFakeProxyInstance(noHTTPProxyValue),
driver: makeFakeDriverInstance(),
},
expectedObjects: testObjects{
driver: makeFakeDriverInstance(),
},
},
{
name: "proxy exists, but observed config is different: new config is observed",
initialObjects: testObjects{
proxy: makeFakeProxyInstance(defaultHTTPProxyValue),
driver: makeFakeDriverInstance(withHTTPProxy(alternativeHTTPProxyValue)),
},
expectedObjects: testObjects{
driver: makeFakeDriverInstance(withHTTPProxy(defaultHTTPProxyValue)),
},
},
}

for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
// Initialize
ctx := newTestContext(test, t)

// Act
err := ctx.controller.Controller.Sync(context.TODO(), factory.NewSyncContext(controllerName, events.NewInMemoryRecorder(operandName)))

// Assert
// Check error
if err != nil && !test.expectErr {
t.Fatalf("sync() returned unexpected error: %v", err)
}
if err == nil && test.expectErr {
t.Fatal("sync() unexpectedly succeeded when error was expected")
}

// Check expectedObjects.driver.Spec
if test.expectedObjects.driver != nil {
actualSpec, _, _, err := ctx.operatorClient.GetOperatorState()
if err != nil {
t.Fatalf("Failed to get Driver: %v", err)
}

if !equality.Semantic.DeepEqual(test.expectedObjects.driver.Spec, *actualSpec) {
t.Fatalf("Unexpected Driver %+v content:\n%s", operandName, cmp.Diff(test.expectedObjects.driver.Spec, *actualSpec))
}
}
})
}
}

// fakeInstance is a fake CSI driver instance that also fullfils the OperatorClient interface
type fakeDriverInstance struct {
metav1.ObjectMeta
Spec opv1.OperatorSpec
Status opv1.OperatorStatus
}
190 changes: 190 additions & 0 deletions pkg/operator/csi/csidrivercontrollerservicecontroller/helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package csidrivercontrollerservicecontroller

import (
"testing"

appsv1 "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"

"github.com/ghodss/yaml"
"github.com/google/go-cmp/cmp"
"github.com/openshift/library-go/pkg/operator/csi/csiconfigobservercontroller"
)

const (
defaultContainerName = "csi-driver"
defaultHTTPProxyValue = "http://foo.bar.proxy"
)

func TestWithObservedProxyDeploymentHook(t *testing.T) {
const (
replica0 = 0
replica1 = 1
replica2 = 2
)
var (
argsLevel2 = 2
)
testCases := []struct {
name string
initialDriver *fakeDriverInstance
initialDeployment *appsv1.Deployment
expectedDeployment *appsv1.Deployment
expectError bool
}{
{
name: "no observed proxy config",
initialDriver: makeFakeDriverInstance(), // CR has no observed proxy config
initialDeployment: makeDeployment(
defaultClusterID,
argsLevel2,
defaultImages(),
withDeploymentHTTPProxyAnnotation(defaultContainerName),
withDeploymentGeneration(1, 0)),
expectedDeployment: makeDeployment( // no container has proxy ENV set
defaultClusterID,
argsLevel2,
defaultImages(),
withDeploymentHTTPProxyAnnotation(defaultContainerName),
withDeploymentGeneration(1, 0)),
expectError: false,
},
{
name: "observed proxy config, annotation present",
initialDriver: makeFakeDriverInstance(
withObservedHTTPProxy(defaultHTTPProxyValue, nil /* config path*/),
),
initialDeployment: makeDeployment(
defaultClusterID,
argsLevel2,
defaultImages(),
withDeploymentHTTPProxyAnnotation(defaultContainerName),
withDeploymentGeneration(1, 0)),
expectedDeployment: makeDeployment(
defaultClusterID,
argsLevel2,
defaultImages(),
withDeploymentGeneration(1, 0),
withDeploymentHTTPProxyAnnotation(defaultContainerName),
withDeploymentHTTPProxyEnv(defaultHTTPProxyValue, defaultContainerName)), // proxy ENV was added to container
expectError: false,
},
{
name: "observed proxy config, annotation present with WRONG container name",
initialDriver: makeFakeDriverInstance(
withObservedHTTPProxy(defaultHTTPProxyValue, nil /* config path*/),
),
initialDeployment: makeDeployment(
defaultClusterID,
argsLevel2,
defaultImages(),
withDeploymentHTTPProxyAnnotation("csi-driver-non-existent"), // this container doesn't exist
withDeploymentGeneration(1, 0)),
expectedDeployment: makeDeployment( // no container has proxy ENV is set
defaultClusterID,
argsLevel2,
defaultImages(),
withDeploymentGeneration(1, 0),
withDeploymentHTTPProxyAnnotation("csi-driver-non-existent")),
expectError: false,
},
{
name: "observed proxy config, annotation NOT present",
initialDriver: makeFakeDriverInstance(
withObservedHTTPProxy(defaultHTTPProxyValue, nil /* config path*/),
),
initialDeployment: makeDeployment( // inject-proxy annotation not added
defaultClusterID,
argsLevel2,
defaultImages(),
withDeploymentGeneration(1, 0)),
expectedDeployment: makeDeployment( // no container has proxy ENV set
defaultClusterID,
argsLevel2,
defaultImages(),
withDeploymentGeneration(1, 0)),
expectError: false,
},
{
name: "invalid observed proxy config",
initialDriver: makeFakeDriverInstance(
withInvalidObservedHTTPProxy(defaultHTTPProxyValue, nil /* config path*/),
),
initialDeployment: makeDeployment(
defaultClusterID,
argsLevel2,
defaultImages(),
withDeploymentHTTPProxyAnnotation(defaultContainerName),
withDeploymentGeneration(1, 0)),
expectedDeployment: makeDeployment( // no container has proxy ENV set
defaultClusterID,
argsLevel2,
defaultImages(),
withDeploymentHTTPProxyAnnotation(defaultContainerName),
withDeploymentGeneration(1, 0)),
expectError: true, // report an error
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
fn := WithObservedProxyDeploymentHook()
err := fn(&tc.initialDriver.Spec, tc.initialDeployment)
if err != nil && !tc.expectError {
t.Errorf("Expected no error running hook function, got: %v", err)

}
if !equality.Semantic.DeepEqual(tc.initialDeployment, tc.expectedDeployment) {
t.Errorf("Unexpected Deployment content:\n%s", cmp.Diff(tc.initialDeployment, tc.expectedDeployment))
}
})
}
}

func withObservedHTTPProxy(proxy string, path []string) driverModifier {
if len(path) == 0 {
path = csiconfigobservercontroller.ProxyConfigPath()
}
return func(i *fakeDriverInstance) *fakeDriverInstance {
observedConfig := map[string]interface{}{}
unstructured.SetNestedStringMap(observedConfig, map[string]string{"HTTP_PROXY": proxy}, path...)
d, _ := yaml.Marshal(observedConfig)
i.Spec.ObservedConfig = runtime.RawExtension{Raw: d, Object: &unstructured.Unstructured{Object: observedConfig}}
return i
}
}

func withInvalidObservedHTTPProxy(proxy string, path []string) driverModifier {
if len(path) == 0 {
path = csiconfigobservercontroller.ProxyConfigPath()
}
return func(i *fakeDriverInstance) *fakeDriverInstance {
observedConfig := map[string]interface{}{}
unstructured.SetNestedStringMap(observedConfig, map[string]string{"HTTP_PROXY": proxy}, path...)
invalidYAML := []byte("[observedConfig:")
i.Spec.ObservedConfig = runtime.RawExtension{Raw: invalidYAML, Object: &unstructured.Unstructured{Object: observedConfig}}
return i
}
}

func withDeploymentHTTPProxyAnnotation(containerName string) deploymentModifier {
return func(instance *appsv1.Deployment) *appsv1.Deployment {
instance.Annotations = map[string]string{"config.openshift.io/inject-proxy": containerName}
return instance
}
}

func withDeploymentHTTPProxyEnv(proxy, containerName string) deploymentModifier {
return func(instance *appsv1.Deployment) *appsv1.Deployment {
containers := instance.Spec.Template.Spec.Containers
for i := range containers {
if containers[i].Name == containerName {
containers[i].Env = append(containers[i].Env, v1.EnvVar{Name: "HTTP_PROXY", Value: proxy})
}
}
return instance
}
}
Loading