Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unit test install strategy ALM-227 #132

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 5 additions & 4 deletions Makefile
Expand Up @@ -65,9 +65,10 @@ $(MOCKGEN):
go get github.com/golang/mock/mockgen

generate-mock-client: $(MOCKGEN)
@$(MOCKGEN) -package=client -source=client/clusterserviceversion_client.go > client/zz_generated.mock_clusterserviceversion_client.go
@$(MOCKGEN) -package=client -source=client/installplan_client.go > client/zz_generated.mock_installplan_client.go
@$(MOCKGEN) -package=client -source=client/alphacatalogentry_client.go > client/zz_generated.mock_alphacatalogentry_client.go
@$(MOCKGEN) -package=install -source=install/resolver.go > install/zz_generated.mock_resolver.go
mockgen -package=client -source=client/clusterserviceversion_client.go > client/zz_generated.mock_clusterserviceversion_client.go
mockgen -package=client -source=client/installplan_client.go > client/zz_generated.mock_installplan_client.go
mockgen -package=client -source=client/alphacatalogentry_client.go > client/zz_generated.mock_alphacatalogentry_client.go
mockgen -package=client -source=client/deployment_install_client.go > client/zz_generated.mock_deployment_install_client.go
mockgen -package=install -source=install/resolver.go > install/zz_generated.mock_resolver.go

make gen-all: gen-ci codegen generate-mock-client
1 change: 1 addition & 0 deletions apis/clusterserviceversion/v1alpha1/types.go
Expand Up @@ -130,6 +130,7 @@ const (
CSVReasonRequirementsNotMet ConditionReason = "RequirementsNotMet"
CSVReasonRequirementsMet ConditionReason = "AllRequirementsMet"
CSVReasonComponentFailed ConditionReason = "InstallComponentFailed"
CSVReasonInvalidStrategy ConditionReason = "InstallStrategyInvalid"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InstallStraegyInvalid reads a little weird to me. Can we reverse the order on it, i.e. InvalidInstallStrategy?

CSVReasonInstallSuccessful ConditionReason = "InstallSucceeded"
CSVReasonInstallCheckFailed ConditionReason = "InstallCheckFailed"
CSVReasonComponentUnhealthy ConditionReason = "ComponentUnhealthy"
Expand Down
90 changes: 90 additions & 0 deletions client/deployment_install_client.go
@@ -0,0 +1,90 @@
package client

import (
"fmt"

opClient "github.com/coreos-inc/operator-client/pkg/client"
log "github.com/sirupsen/logrus"
"github.com/pkg/errors"
"k8s.io/api/core/v1"
v1beta1extensions "k8s.io/api/extensions/v1beta1"
v1beta1rbac "k8s.io/api/rbac/v1beta1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

type InstallStrategyDeploymentInterface interface {
CreateRole(role *v1beta1rbac.Role) (*v1beta1rbac.Role, error)
CreateRoleBinding(roleBinding *v1beta1rbac.RoleBinding) (*v1beta1rbac.RoleBinding, error)
GetOrCreateServiceAccount(serviceAccount *v1.ServiceAccount) (*v1.ServiceAccount, error)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expected behavior of this function isn't very clear from the name. I think simply making it CreateServiceAccount where it's a no-op if it already exists (or EnsureServiceCountExists?) is fine.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It actually does return the ServiceAccount it finds if it's already there - it's not a no-op if it already exists.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It didn't look like anything other than the name, which is set in alm/install/deployment and by necessity the same.

Should this really be an upsert, i.e. CreateOrUpdateServiceAccount? I think what's throwing me off is maybe the service account will match what you gave the function, maybe it won't (if it already exists).

CreateDeployment(deployment *v1beta1extensions.Deployment) (*v1beta1extensions.Deployment, error)
CheckServiceAccount(serviceAccountName string) (bool, error)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check what? It might be simpler to just have this be the simple GetServiceAccountByName, otherwise maybe CheckServiceAccountExists or something else to clarify what this means.

CheckOwnedDeployments(owner metav1.ObjectMeta, deploymentSpecs []v1beta1extensions.DeploymentSpec) (bool, error)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. This function weirdly mixes client and consumer concerns. Doing GetOwnedDeployments and having the compare logic in install/deployment.go makes it much easier to understand what's going on.

}

type InstallStrategyDeploymentClient struct {
opClient opClient.Interface
Namespace string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unclear on how this namespace is used. Are all actions scoped to this namespace? As a consumer of this client, it's very opaque. Either consistently use Namespace and rename this like InstallStrategyDeploymentClientForNamespace or whatever, or (my preference) add namespace string as an argument to each function in the interface that needs it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, all actions are restricted to the namespace. I can make that clear with the name

}

var _ InstallStrategyDeploymentInterface = &InstallStrategyDeploymentClient{}

func NewInstallStrategyDeploymentClient(opClient opClient.Interface, namespace string) InstallStrategyDeploymentInterface {
return &InstallStrategyDeploymentClient{
opClient: opClient,
Namespace: namespace,
}
}

func (c *InstallStrategyDeploymentClient) CreateRole(role *v1beta1rbac.Role) (*v1beta1rbac.Role, error) {
return c.opClient.KubernetesInterface().RbacV1beta1().Roles(c.Namespace).Create(role)
}

func (c *InstallStrategyDeploymentClient) CreateRoleBinding(roleBinding *v1beta1rbac.RoleBinding) (*v1beta1rbac.RoleBinding, error) {
return c.opClient.KubernetesInterface().RbacV1beta1().RoleBindings(c.Namespace).Create(roleBinding)
}

func (c *InstallStrategyDeploymentClient) GetOrCreateServiceAccount(serviceAccount *v1.ServiceAccount) (*v1.ServiceAccount, error) {
foundAccount, err := c.opClient.KubernetesInterface().CoreV1().ServiceAccounts(c.Namespace).Get(serviceAccount.Name, metav1.GetOptions{})
if err != nil && !apierrors.IsNotFound(err) {
return nil, errors.Wrap(err, "checking for existing serviceacccount failed")
}
if foundAccount != nil {
return foundAccount, nil
}

createdAccount, err := c.opClient.KubernetesInterface().CoreV1().ServiceAccounts(c.Namespace).Create(serviceAccount)
if err != nil && !apierrors.IsAlreadyExists(err) {
return createdAccount, errors.Wrap(err, "creating serviceacccount failed")
}
return createdAccount, nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need to re-fetch the serviceaccount if you get a IsAlreadyExists error?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, we only need the name to create the rolebinding

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But won't createdAccount be nil? You could return serviceAccount but I still find it unclear about what you're getting back. In the first Get, the return value foundAccount != serviceAccount necessarily. And if the Create is successful, the createdAccount will have additional data on it that serviceAccount does not. I'd rather have consistent behavior, which might mean updating the service account if found.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will be nil, but that's the correct value for it for this return. I've changed it explicitly to nil so that it's clear.

}

func (c *InstallStrategyDeploymentClient) CreateDeployment(deployment *v1beta1extensions.Deployment) (*v1beta1extensions.Deployment, error) {
return c.opClient.CreateDeployment(deployment)
}

func (c *InstallStrategyDeploymentClient) CheckServiceAccount(serviceAccountName string) (bool, error) {
if _, err := c.opClient.KubernetesInterface().CoreV1().ServiceAccounts(c.Namespace).Get(serviceAccountName, metav1.GetOptions{}); err != nil {
if apierrors.IsNotFound(err) {
return false, nil
}
return false, fmt.Errorf("serviceaccount %s not found: %s", serviceAccountName, err.Error())
}
return true, nil
}

func (c *InstallStrategyDeploymentClient) CheckOwnedDeployments(owner metav1.ObjectMeta, deploymentSpecs []v1beta1extensions.DeploymentSpec) (bool, error) {
existingDeployments, err := c.opClient.ListDeploymentsWithLabels(c.Namespace, map[string]string{
"alm-owner-name": owner.Name,
"alm-owner-namespace": owner.Namespace,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personal pref, I would prefer to have CheckOwnedDeployments(ownerName, ownerNamespace...) or whatever to clarify what it's matching on.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might change this query to user ownerreferences in the future, so I think it makes sense to keep the whole owner

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I have some fear still from experiences where parts of the codebase break unexpectedly when you change something about the object because it relied on some obscure field on the object you didn't even know about.

I'll trust your judgement on where we need flexibility more than guaranteeing future stability. 👍

})
if err != nil {
return false, fmt.Errorf("couldn't query for existing deployments: %s", err)
}
if len(existingDeployments.Items) != len(deploymentSpecs) {
log.Debugf("wrong number of deployments found. want %d, got %d", len(deploymentSpecs), len(existingDeployments.Items))
return false, nil
}
return true, nil
}
40 changes: 16 additions & 24 deletions client/zz_generated.mock_clusterserviceversion_client.go
@@ -1,59 +1,51 @@
// Code generated by MockGen. DO NOT EDIT.
// Automatically generated by MockGen. DO NOT EDIT!
// Source: client/clusterserviceversion_client.go

// Package client is a generated GoMock package.
package client

import (
v1alpha1 "github.com/coreos-inc/alm/apis/clusterserviceversion/v1alpha1"
gomock "github.com/golang/mock/gomock"
reflect "reflect"
)

// MockClusterServiceVersionInterface is a mock of ClusterServiceVersionInterface interface
// Mock of ClusterServiceVersionInterface interface
type MockClusterServiceVersionInterface struct {
ctrl *gomock.Controller
recorder *MockClusterServiceVersionInterfaceMockRecorder
recorder *_MockClusterServiceVersionInterfaceRecorder
}

// MockClusterServiceVersionInterfaceMockRecorder is the mock recorder for MockClusterServiceVersionInterface
type MockClusterServiceVersionInterfaceMockRecorder struct {
// Recorder for MockClusterServiceVersionInterface (not exported)
type _MockClusterServiceVersionInterfaceRecorder struct {
mock *MockClusterServiceVersionInterface
}

// NewMockClusterServiceVersionInterface creates a new mock instance
func NewMockClusterServiceVersionInterface(ctrl *gomock.Controller) *MockClusterServiceVersionInterface {
mock := &MockClusterServiceVersionInterface{ctrl: ctrl}
mock.recorder = &MockClusterServiceVersionInterfaceMockRecorder{mock}
mock.recorder = &_MockClusterServiceVersionInterfaceRecorder{mock}
return mock
}

// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockClusterServiceVersionInterface) EXPECT() *MockClusterServiceVersionInterfaceMockRecorder {
return m.recorder
func (_m *MockClusterServiceVersionInterface) EXPECT() *_MockClusterServiceVersionInterfaceRecorder {
return _m.recorder
}

// UpdateCSV mocks base method
func (m *MockClusterServiceVersionInterface) UpdateCSV(csv *v1alpha1.ClusterServiceVersion) (*v1alpha1.ClusterServiceVersion, error) {
ret := m.ctrl.Call(m, "UpdateCSV", csv)
func (_m *MockClusterServiceVersionInterface) UpdateCSV(csv *v1alpha1.ClusterServiceVersion) (*v1alpha1.ClusterServiceVersion, error) {
ret := _m.ctrl.Call(_m, "UpdateCSV", csv)
ret0, _ := ret[0].(*v1alpha1.ClusterServiceVersion)
ret1, _ := ret[1].(error)
return ret0, ret1
}

// UpdateCSV indicates an expected call of UpdateCSV
func (mr *MockClusterServiceVersionInterfaceMockRecorder) UpdateCSV(csv interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateCSV", reflect.TypeOf((*MockClusterServiceVersionInterface)(nil).UpdateCSV), csv)
func (_mr *_MockClusterServiceVersionInterfaceRecorder) UpdateCSV(arg0 interface{}) *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "UpdateCSV", arg0)
}

// CreateCSV mocks base method
func (m *MockClusterServiceVersionInterface) CreateCSV(csv *v1alpha1.ClusterServiceVersion) error {
ret := m.ctrl.Call(m, "CreateCSV", csv)
func (_m *MockClusterServiceVersionInterface) CreateCSV(csv *v1alpha1.ClusterServiceVersion) error {
ret := _m.ctrl.Call(_m, "CreateCSV", csv)
ret0, _ := ret[0].(error)
return ret0
}

// CreateCSV indicates an expected call of CreateCSV
func (mr *MockClusterServiceVersionInterfaceMockRecorder) CreateCSV(csv interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCSV", reflect.TypeOf((*MockClusterServiceVersionInterface)(nil).CreateCSV), csv)
func (_mr *_MockClusterServiceVersionInterfaceRecorder) CreateCSV(arg0 interface{}) *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "CreateCSV", arg0)
}
99 changes: 99 additions & 0 deletions client/zz_generated.mock_deployment_install_client.go
@@ -0,0 +1,99 @@
// Automatically generated by MockGen. DO NOT EDIT!
// Source: client/deployment_install_client.go

package client

import (
gomock "github.com/golang/mock/gomock"
v1 "k8s.io/api/core/v1"
v1beta10 "k8s.io/api/extensions/v1beta1"
v1beta1 "k8s.io/api/rbac/v1beta1"
v10 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// Mock of InstallStrategyDeploymentInterface interface
type MockInstallStrategyDeploymentInterface struct {
ctrl *gomock.Controller
recorder *_MockInstallStrategyDeploymentInterfaceRecorder
}

// Recorder for MockInstallStrategyDeploymentInterface (not exported)
type _MockInstallStrategyDeploymentInterfaceRecorder struct {
mock *MockInstallStrategyDeploymentInterface
}

func NewMockInstallStrategyDeploymentInterface(ctrl *gomock.Controller) *MockInstallStrategyDeploymentInterface {
mock := &MockInstallStrategyDeploymentInterface{ctrl: ctrl}
mock.recorder = &_MockInstallStrategyDeploymentInterfaceRecorder{mock}
return mock
}

func (_m *MockInstallStrategyDeploymentInterface) EXPECT() *_MockInstallStrategyDeploymentInterfaceRecorder {
return _m.recorder
}

func (_m *MockInstallStrategyDeploymentInterface) CreateRole(role *v1beta1.Role) (*v1beta1.Role, error) {
ret := _m.ctrl.Call(_m, "CreateRole", role)
ret0, _ := ret[0].(*v1beta1.Role)
ret1, _ := ret[1].(error)
return ret0, ret1
}

func (_mr *_MockInstallStrategyDeploymentInterfaceRecorder) CreateRole(arg0 interface{}) *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "CreateRole", arg0)
}

func (_m *MockInstallStrategyDeploymentInterface) CreateRoleBinding(roleBinding *v1beta1.RoleBinding) (*v1beta1.RoleBinding, error) {
ret := _m.ctrl.Call(_m, "CreateRoleBinding", roleBinding)
ret0, _ := ret[0].(*v1beta1.RoleBinding)
ret1, _ := ret[1].(error)
return ret0, ret1
}

func (_mr *_MockInstallStrategyDeploymentInterfaceRecorder) CreateRoleBinding(arg0 interface{}) *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "CreateRoleBinding", arg0)
}

func (_m *MockInstallStrategyDeploymentInterface) GetOrCreateServiceAccount(serviceAccount *v1.ServiceAccount) (*v1.ServiceAccount, error) {
ret := _m.ctrl.Call(_m, "GetOrCreateServiceAccount", serviceAccount)
ret0, _ := ret[0].(*v1.ServiceAccount)
ret1, _ := ret[1].(error)
return ret0, ret1
}

func (_mr *_MockInstallStrategyDeploymentInterfaceRecorder) GetOrCreateServiceAccount(arg0 interface{}) *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "GetOrCreateServiceAccount", arg0)
}

func (_m *MockInstallStrategyDeploymentInterface) CreateDeployment(deployment *v1beta10.Deployment) (*v1beta10.Deployment, error) {
ret := _m.ctrl.Call(_m, "CreateDeployment", deployment)
ret0, _ := ret[0].(*v1beta10.Deployment)
ret1, _ := ret[1].(error)
return ret0, ret1
}

func (_mr *_MockInstallStrategyDeploymentInterfaceRecorder) CreateDeployment(arg0 interface{}) *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "CreateDeployment", arg0)
}

func (_m *MockInstallStrategyDeploymentInterface) CheckServiceAccount(serviceAccountName string) (bool, error) {
ret := _m.ctrl.Call(_m, "CheckServiceAccount", serviceAccountName)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}

func (_mr *_MockInstallStrategyDeploymentInterfaceRecorder) CheckServiceAccount(arg0 interface{}) *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "CheckServiceAccount", arg0)
}

func (_m *MockInstallStrategyDeploymentInterface) CheckOwnedDeployments(owner v10.ObjectMeta, deploymentSpecs []v1beta10.DeploymentSpec) (bool, error) {
ret := _m.ctrl.Call(_m, "CheckOwnedDeployments", owner, deploymentSpecs)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}

func (_mr *_MockInstallStrategyDeploymentInterfaceRecorder) CheckOwnedDeployments(arg0, arg1 interface{}) *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "CheckOwnedDeployments", arg0, arg1)
}
30 changes: 12 additions & 18 deletions client/zz_generated.mock_installplan_client.go
@@ -1,47 +1,41 @@
// Code generated by MockGen. DO NOT EDIT.
// Automatically generated by MockGen. DO NOT EDIT!
// Source: client/installplan_client.go

// Package client is a generated GoMock package.
package client

import (
v1alpha1 "github.com/coreos-inc/alm/apis/installplan/v1alpha1"
gomock "github.com/golang/mock/gomock"
reflect "reflect"
)

// MockInstallPlanInterface is a mock of InstallPlanInterface interface
// Mock of InstallPlanInterface interface
type MockInstallPlanInterface struct {
ctrl *gomock.Controller
recorder *MockInstallPlanInterfaceMockRecorder
recorder *_MockInstallPlanInterfaceRecorder
}

// MockInstallPlanInterfaceMockRecorder is the mock recorder for MockInstallPlanInterface
type MockInstallPlanInterfaceMockRecorder struct {
// Recorder for MockInstallPlanInterface (not exported)
type _MockInstallPlanInterfaceRecorder struct {
mock *MockInstallPlanInterface
}

// NewMockInstallPlanInterface creates a new mock instance
func NewMockInstallPlanInterface(ctrl *gomock.Controller) *MockInstallPlanInterface {
mock := &MockInstallPlanInterface{ctrl: ctrl}
mock.recorder = &MockInstallPlanInterfaceMockRecorder{mock}
mock.recorder = &_MockInstallPlanInterfaceRecorder{mock}
return mock
}

// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockInstallPlanInterface) EXPECT() *MockInstallPlanInterfaceMockRecorder {
return m.recorder
func (_m *MockInstallPlanInterface) EXPECT() *_MockInstallPlanInterfaceRecorder {
return _m.recorder
}

// UpdateInstallPlan mocks base method
func (m *MockInstallPlanInterface) UpdateInstallPlan(arg0 *v1alpha1.InstallPlan) (*v1alpha1.InstallPlan, error) {
ret := m.ctrl.Call(m, "UpdateInstallPlan", arg0)
func (_m *MockInstallPlanInterface) UpdateInstallPlan(_param0 *v1alpha1.InstallPlan) (*v1alpha1.InstallPlan, error) {
ret := _m.ctrl.Call(_m, "UpdateInstallPlan", _param0)
ret0, _ := ret[0].(*v1alpha1.InstallPlan)
ret1, _ := ret[1].(error)
return ret0, ret1
}

// UpdateInstallPlan indicates an expected call of UpdateInstallPlan
func (mr *MockInstallPlanInterfaceMockRecorder) UpdateInstallPlan(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateInstallPlan", reflect.TypeOf((*MockInstallPlanInterface)(nil).UpdateInstallPlan), arg0)
func (_mr *_MockInstallPlanInterfaceRecorder) UpdateInstallPlan(arg0 interface{}) *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "UpdateInstallPlan", arg0)
}