Feature/unit tests (#53)
- Avoid relying on Clientset structure to call Kubernetes API functions. While Clientset is a convinient "catch-all" abstraction for calling REST API related to different Kubernetes objects, it's impossible to mock. Replacing it wih the kubernetes.Interface would be quite straightforward, but would require an exra level of mocked interfaces, because of the versioning. Instead, a new interface is defined, which contains only the objects we need of the pre-defined versions. - Move KubernetesClient to k8sutil package. - Add more tests.
This commit is contained in:
parent
4f36e447c3
commit
4455f1b639
|
|
@ -48,7 +48,7 @@ func ControllerConfig() *controller.Config {
|
||||||
log.Fatalf("Can't get REST config: %s", err)
|
log.Fatalf("Can't get REST config: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := k8sutil.KubernetesClient(restConfig)
|
client, err := k8sutil.ClientSet(restConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Can't create client: %s", err)
|
log.Fatalf("Can't create client: %s", err)
|
||||||
}
|
}
|
||||||
|
|
@ -60,7 +60,7 @@ func ControllerConfig() *controller.Config {
|
||||||
|
|
||||||
return &controller.Config{
|
return &controller.Config{
|
||||||
RestConfig: restConfig,
|
RestConfig: restConfig,
|
||||||
KubeClient: client,
|
KubeClient: k8sutil.NewFromKubernetesInterface(client),
|
||||||
RestClient: restClient,
|
RestClient: restClient,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -101,7 +101,7 @@ func main() {
|
||||||
|
|
||||||
log.Printf("Config: %s", cfg.MustMarshal())
|
log.Printf("Config: %s", cfg.MustMarshal())
|
||||||
|
|
||||||
c := controller.New(controllerConfig, cfg)
|
c := controller.NewController(controllerConfig, cfg)
|
||||||
c.Run(stop, wg)
|
c.Run(stop, wg)
|
||||||
|
|
||||||
sig := <-sigs
|
sig := <-sigs
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
"k8s.io/client-go/kubernetes"
|
|
||||||
"k8s.io/client-go/pkg/api"
|
"k8s.io/client-go/pkg/api"
|
||||||
"k8s.io/client-go/pkg/api/v1"
|
"k8s.io/client-go/pkg/api/v1"
|
||||||
"k8s.io/client-go/pkg/apis/apps/v1beta1"
|
"k8s.io/client-go/pkg/apis/apps/v1beta1"
|
||||||
|
|
@ -36,7 +35,7 @@ var (
|
||||||
|
|
||||||
// Config contains operator-wide clients and configuration used from a cluster. TODO: remove struct duplication.
|
// Config contains operator-wide clients and configuration used from a cluster. TODO: remove struct duplication.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
KubeClient *kubernetes.Clientset //TODO: move clients to the better place?
|
KubeClient k8sutil.KubernetesClient
|
||||||
RestClient *rest.RESTClient
|
RestClient *rest.RESTClient
|
||||||
RestConfig *rest.Config
|
RestConfig *rest.Config
|
||||||
TeamsAPIClient *teams.API
|
TeamsAPIClient *teams.API
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
"k8s.io/client-go/kubernetes"
|
|
||||||
"k8s.io/client-go/pkg/api/v1"
|
"k8s.io/client-go/pkg/api/v1"
|
||||||
"k8s.io/client-go/rest"
|
"k8s.io/client-go/rest"
|
||||||
"k8s.io/client-go/tools/cache"
|
"k8s.io/client-go/tools/cache"
|
||||||
|
|
@ -14,12 +13,13 @@ import (
|
||||||
"github.com/zalando-incubator/postgres-operator/pkg/spec"
|
"github.com/zalando-incubator/postgres-operator/pkg/spec"
|
||||||
"github.com/zalando-incubator/postgres-operator/pkg/util/config"
|
"github.com/zalando-incubator/postgres-operator/pkg/util/config"
|
||||||
"github.com/zalando-incubator/postgres-operator/pkg/util/constants"
|
"github.com/zalando-incubator/postgres-operator/pkg/util/constants"
|
||||||
|
"github.com/zalando-incubator/postgres-operator/pkg/util/k8sutil"
|
||||||
"github.com/zalando-incubator/postgres-operator/pkg/util/teams"
|
"github.com/zalando-incubator/postgres-operator/pkg/util/teams"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
RestConfig *rest.Config
|
RestConfig *rest.Config
|
||||||
KubeClient *kubernetes.Clientset
|
KubeClient k8sutil.KubernetesClient
|
||||||
RestClient *rest.RESTClient
|
RestClient *rest.RESTClient
|
||||||
TeamsAPIClient *teams.API
|
TeamsAPIClient *teams.API
|
||||||
InfrastructureRoles map[string]spec.PgUser
|
InfrastructureRoles map[string]spec.PgUser
|
||||||
|
|
@ -27,6 +27,7 @@ type Config struct {
|
||||||
|
|
||||||
type Controller struct {
|
type Controller struct {
|
||||||
Config
|
Config
|
||||||
|
|
||||||
opConfig *config.Config
|
opConfig *config.Config
|
||||||
logger *logrus.Entry
|
logger *logrus.Entry
|
||||||
|
|
||||||
|
|
@ -43,7 +44,7 @@ type Controller struct {
|
||||||
lastClusterSyncTime int64
|
lastClusterSyncTime int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(controllerConfig *Config, operatorConfig *config.Config) *Controller {
|
func NewController(controllerConfig *Config, operatorConfig *config.Config) *Controller {
|
||||||
logger := logrus.New()
|
logger := logrus.New()
|
||||||
|
|
||||||
if operatorConfig.DebugLogging {
|
if operatorConfig.DebugLogging {
|
||||||
|
|
@ -82,7 +83,7 @@ func (c *Controller) initController() {
|
||||||
c.logger.Fatalf("could not register ThirdPartyResource: %v", err)
|
c.logger.Fatalf("could not register ThirdPartyResource: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if infraRoles, err := c.getInfrastructureRoles(); err != nil {
|
if infraRoles, err := c.getInfrastructureRoles(&c.opConfig.InfrastructureRolesSecretName); err != nil {
|
||||||
c.logger.Warningf("could not get infrastructure roles: %v", err)
|
c.logger.Warningf("could not get infrastructure roles: %v", err)
|
||||||
} else {
|
} else {
|
||||||
c.InfrastructureRoles = infraRoles
|
c.InfrastructureRoles = infraRoles
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ func (c *Controller) podListFunc(options api.ListOptions) (runtime.Object, error
|
||||||
TimeoutSeconds: options.TimeoutSeconds,
|
TimeoutSeconds: options.TimeoutSeconds,
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.KubeClient.CoreV1().Pods(c.opConfig.Namespace).List(opts)
|
return c.KubeClient.Pods(c.opConfig.Namespace).List(opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) podWatchFunc(options api.ListOptions) (watch.Interface, error) {
|
func (c *Controller) podWatchFunc(options api.ListOptions) (watch.Interface, error) {
|
||||||
|
|
@ -52,7 +52,7 @@ func (c *Controller) podWatchFunc(options api.ListOptions) (watch.Interface, err
|
||||||
TimeoutSeconds: options.TimeoutSeconds,
|
TimeoutSeconds: options.TimeoutSeconds,
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.KubeClient.CoreV1Client.Pods(c.opConfig.Namespace).Watch(opts)
|
return c.KubeClient.Pods(c.opConfig.Namespace).Watch(opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) podAdd(obj interface{}) {
|
func (c *Controller) podAdd(obj interface{}) {
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ func (c *Controller) createTPR() error {
|
||||||
TPRName := fmt.Sprintf("%s.%s", constants.TPRName, constants.TPRVendor)
|
TPRName := fmt.Sprintf("%s.%s", constants.TPRName, constants.TPRVendor)
|
||||||
tpr := thirdPartyResource(TPRName)
|
tpr := thirdPartyResource(TPRName)
|
||||||
|
|
||||||
_, err := c.KubeClient.ExtensionsV1beta1().ThirdPartyResources().Create(tpr)
|
_, err := c.KubeClient.ThirdPartyResources().Create(tpr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !k8sutil.ResourceAlreadyExists(err) {
|
if !k8sutil.ResourceAlreadyExists(err) {
|
||||||
return err
|
return err
|
||||||
|
|
@ -64,17 +64,17 @@ func (c *Controller) createTPR() error {
|
||||||
return k8sutil.WaitTPRReady(c.RestClient, c.opConfig.TPR.ReadyWaitInterval, c.opConfig.TPR.ReadyWaitTimeout, c.opConfig.Namespace)
|
return k8sutil.WaitTPRReady(c.RestClient, c.opConfig.TPR.ReadyWaitInterval, c.opConfig.TPR.ReadyWaitTimeout, c.opConfig.Namespace)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) getInfrastructureRoles() (result map[string]spec.PgUser, err error) {
|
func (c *Controller) getInfrastructureRoles(rolesSecret *spec.NamespacedName) (result map[string]spec.PgUser, err error) {
|
||||||
if c.opConfig.InfrastructureRolesSecretName == (spec.NamespacedName{}) {
|
if *rolesSecret == (spec.NamespacedName{}) {
|
||||||
// we don't have infrastructure roles defined, bail out
|
// we don't have infrastructure roles defined, bail out
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
infraRolesSecret, err := c.KubeClient.
|
infraRolesSecret, err := c.KubeClient.
|
||||||
Secrets(c.opConfig.InfrastructureRolesSecretName.Namespace).
|
Secrets(rolesSecret.Namespace).
|
||||||
Get(c.opConfig.InfrastructureRolesSecretName.Name)
|
Get(rolesSecret.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Debugf("Infrastructure roles secret name: %s", c.opConfig.InfrastructureRolesSecretName)
|
c.logger.Debugf("Infrastructure roles secret name: %s", *rolesSecret)
|
||||||
return nil, fmt.Errorf("could not get infrastructure roles secret: %v", err)
|
return nil, fmt.Errorf("could not get infrastructure roles secret: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
v1core "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||||
|
"k8s.io/client-go/pkg/api/v1"
|
||||||
|
|
||||||
|
"github.com/zalando-incubator/postgres-operator/pkg/spec"
|
||||||
|
"github.com/zalando-incubator/postgres-operator/pkg/util/config"
|
||||||
|
"github.com/zalando-incubator/postgres-operator/pkg/util/k8sutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
testInfrastructureRolesSecretName = "infrastructureroles-test"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockSecret struct {
|
||||||
|
v1core.SecretInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *mockSecret) Get(name string) (*v1.Secret, error) {
|
||||||
|
if name != testInfrastructureRolesSecretName {
|
||||||
|
return nil, fmt.Errorf("NotFound")
|
||||||
|
}
|
||||||
|
secret := &v1.Secret{}
|
||||||
|
secret.Name = mockController.opConfig.ClusterNameLabel
|
||||||
|
secret.Data = map[string][]byte{
|
||||||
|
"user1": []byte("testrole"),
|
||||||
|
"password1": []byte("testpassword"),
|
||||||
|
"inrole1": []byte("testinrole"),
|
||||||
|
}
|
||||||
|
return secret, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockSecretGetter struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MockSecretGetter) Secrets(namespace string) v1core.SecretInterface {
|
||||||
|
return &mockSecret{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockKubernetesClient() k8sutil.KubernetesClient {
|
||||||
|
return k8sutil.KubernetesClient{SecretsGetter: &MockSecretGetter{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockController() *Controller {
|
||||||
|
controller := NewController(&Config{}, &config.Config{})
|
||||||
|
controller.opConfig.ClusterNameLabel = "cluster-name"
|
||||||
|
controller.opConfig.InfrastructureRolesSecretName =
|
||||||
|
spec.NamespacedName{v1.NamespaceDefault, testInfrastructureRolesSecretName}
|
||||||
|
controller.opConfig.Workers = 4
|
||||||
|
controller.KubeClient = newMockKubernetesClient()
|
||||||
|
return controller
|
||||||
|
}
|
||||||
|
|
||||||
|
var mockController = newMockController()
|
||||||
|
|
||||||
|
func TestPodClusterName(t *testing.T) {
|
||||||
|
var testTable = []struct {
|
||||||
|
in *v1.Pod
|
||||||
|
expected spec.NamespacedName
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
&v1.Pod{},
|
||||||
|
spec.NamespacedName{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
&v1.Pod{
|
||||||
|
ObjectMeta: v1.ObjectMeta{
|
||||||
|
Namespace: v1.NamespaceDefault,
|
||||||
|
Labels: map[string]string{
|
||||||
|
mockController.opConfig.ClusterNameLabel: "testcluster",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
spec.NamespacedName{v1.NamespaceDefault, "testcluster"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range testTable {
|
||||||
|
resp := mockController.podClusterName(test.in)
|
||||||
|
if resp != test.expected {
|
||||||
|
t.Errorf("expected response %v does not match the actual %v", test.expected, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClusterWorkerID(t *testing.T) {
|
||||||
|
var testTable = []struct {
|
||||||
|
in spec.NamespacedName
|
||||||
|
expected uint32
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
in: spec.NamespacedName{"foo", "bar"},
|
||||||
|
expected: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: spec.NamespacedName{"default", "testcluster"},
|
||||||
|
expected: 3,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range testTable {
|
||||||
|
resp := mockController.clusterWorkerID(test.in)
|
||||||
|
if resp != test.expected {
|
||||||
|
t.Errorf("expected response %v does not match the actual %v", test.expected, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetInfrastructureRoles(t *testing.T) {
|
||||||
|
var testTable = []struct {
|
||||||
|
secretName spec.NamespacedName
|
||||||
|
expectedRoles map[string]spec.PgUser
|
||||||
|
expectedError error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
spec.NamespacedName{},
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
spec.NamespacedName{v1.NamespaceDefault, "null"},
|
||||||
|
nil,
|
||||||
|
fmt.Errorf(`could not get infrastructure roles secret: NotFound`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
spec.NamespacedName{v1.NamespaceDefault, testInfrastructureRolesSecretName},
|
||||||
|
map[string]spec.PgUser{
|
||||||
|
"testrole": {
|
||||||
|
"testrole",
|
||||||
|
"testpassword",
|
||||||
|
nil,
|
||||||
|
[]string{"testinrole"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range testTable {
|
||||||
|
roles, err := mockController.getInfrastructureRoles(&test.secretName)
|
||||||
|
if err != test.expectedError {
|
||||||
|
if err != nil && test.expectedError != nil && err.Error() == test.expectedError.Error() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t.Errorf("expected error '%v' does not match the actual error '%v'", test.expectedError, err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(roles, test.expectedRoles) {
|
||||||
|
t.Errorf("expected roles output %v does not match the actual %v", test.expectedRoles, roles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,6 @@ package spec
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
||||||
"k8s.io/client-go/pkg/api/v1"
|
"k8s.io/client-go/pkg/api/v1"
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
|
v1beta1 "k8s.io/client-go/kubernetes/typed/apps/v1beta1"
|
||||||
|
v1core "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||||
|
extensions "k8s.io/client-go/kubernetes/typed/extensions/v1beta1"
|
||||||
"k8s.io/client-go/pkg/api"
|
"k8s.io/client-go/pkg/api"
|
||||||
apierrors "k8s.io/client-go/pkg/api/errors"
|
apierrors "k8s.io/client-go/pkg/api/errors"
|
||||||
"k8s.io/client-go/pkg/api/unversioned"
|
"k8s.io/client-go/pkg/api/unversioned"
|
||||||
|
|
@ -18,6 +21,32 @@ import (
|
||||||
"github.com/zalando-incubator/postgres-operator/pkg/util/retryutil"
|
"github.com/zalando-incubator/postgres-operator/pkg/util/retryutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type KubernetesClient struct {
|
||||||
|
v1core.SecretsGetter
|
||||||
|
v1core.ServicesGetter
|
||||||
|
v1core.EndpointsGetter
|
||||||
|
v1core.PodsGetter
|
||||||
|
v1core.PersistentVolumesGetter
|
||||||
|
v1core.PersistentVolumeClaimsGetter
|
||||||
|
v1core.ConfigMapsGetter
|
||||||
|
v1beta1.StatefulSetsGetter
|
||||||
|
extensions.ThirdPartyResourcesGetter
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFromKubernetesInterface(src kubernetes.Interface) (c KubernetesClient) {
|
||||||
|
c = KubernetesClient{}
|
||||||
|
c.PodsGetter = src.CoreV1()
|
||||||
|
c.ServicesGetter = src.CoreV1()
|
||||||
|
c.EndpointsGetter = src.CoreV1()
|
||||||
|
c.SecretsGetter = src.CoreV1()
|
||||||
|
c.ConfigMapsGetter = src.CoreV1()
|
||||||
|
c.PersistentVolumeClaimsGetter = src.CoreV1()
|
||||||
|
c.PersistentVolumesGetter = src.CoreV1()
|
||||||
|
c.StatefulSetsGetter = src.AppsV1beta1()
|
||||||
|
c.ThirdPartyResourcesGetter = src.ExtensionsV1beta1()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func RestConfig(kubeConfig string, outOfCluster bool) (*rest.Config, error) {
|
func RestConfig(kubeConfig string, outOfCluster bool) (*rest.Config, error) {
|
||||||
if outOfCluster {
|
if outOfCluster {
|
||||||
return clientcmd.BuildConfigFromFlags("", kubeConfig)
|
return clientcmd.BuildConfigFromFlags("", kubeConfig)
|
||||||
|
|
@ -25,7 +54,7 @@ func RestConfig(kubeConfig string, outOfCluster bool) (*rest.Config, error) {
|
||||||
return rest.InClusterConfig()
|
return rest.InClusterConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
func KubernetesClient(config *rest.Config) (client *kubernetes.Clientset, err error) {
|
func ClientSet(config *rest.Config) (client *kubernetes.Clientset, err error) {
|
||||||
return kubernetes.NewForConfig(config)
|
return kubernetes.NewForConfig(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue