From 36a57757784303cb1a3cb7e5effa3b74786244ea Mon Sep 17 00:00:00 2001 From: Isaac Dorfman Date: Tue, 17 Jan 2023 17:43:43 +0200 Subject: [PATCH] Increased unit test coverage --- Makefile | 7 + go.mod | 1 + go.sum | 1 + provider/cluster_resource.go | 235 +++++----- provider/cluster_resource_common.go | 132 ++++++ provider/cluster_resource_mock.go | 209 +++++++++ provider/cluster_resource_test.go | 427 +++++++++++++++--- provider/cluster_resource_test_common.go | 43 ++ .../cluster_rosa_classic_resource_test.go | 20 - 9 files changed, 877 insertions(+), 198 deletions(-) create mode 100644 provider/cluster_resource_common.go create mode 100644 provider/cluster_resource_mock.go create mode 100644 provider/cluster_resource_test_common.go diff --git a/Makefile b/Makefile index 31522b8..febd762 100644 --- a/Makefile +++ b/Makefile @@ -36,10 +36,17 @@ ldflags:=\ -X $(import_path)/build.Commit=$(commit) \ $(NULL) +.PHONY: gen-mocks +gen-mocks: + mockgen -source=provider/cluster_resource.go -destination=provider/cluster_resource_mock.go -package provider + .PHONY: build build: go build -ldflags="$(ldflags)" -o ${BINARY} +generate-mocks: + mockgen -source=provider/cluster_resource.go -destination=provider/cluster_resource_mock.go -package provider + .PHONY: install install: build platform=$$(terraform version -json | jq -r .platform); \ diff --git a/go.mod b/go.mod index 044629e..878854a 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/openshift-online/terraform-provider-ocm go 1.17 require ( + github.com/golang/mock v1.4.4 github.com/hashicorp/go-version v1.3.0 github.com/hashicorp/terraform-plugin-framework v0.5.0 github.com/hashicorp/terraform-plugin-go v0.5.0 diff --git a/go.sum b/go.sum index b8e0996..d29092b 100644 --- a/go.sum +++ b/go.sum @@ -106,6 +106,7 @@ github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFU github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= diff --git a/provider/cluster_resource.go b/provider/cluster_resource.go index d48b2a8..4f66064 100644 --- a/provider/cluster_resource.go +++ b/provider/cluster_resource.go @@ -19,24 +19,15 @@ package provider import ( "context" "fmt" - "net/http" - "time" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1" - "github.com/openshift-online/ocm-sdk-go/errors" - "github.com/openshift-online/ocm-sdk-go/logging" ) - -type ClusterResourceType struct { -} - -type ClusterResource struct { - logger logging.Logger - collection *cmv1.ClustersClient +type ClusterResourceUtilsImpl struct { + clusterClient ClusterClient } func (t *ClusterResourceType) GetSchema(ctx context.Context) (result tfsdk.Schema, @@ -243,8 +234,8 @@ func (t *ClusterResourceType) NewResource(ctx context.Context, return } -func createClusterObject(ctx context.Context, - state *ClusterState, diags diag.Diagnostics) (*cmv1.Cluster, error) { +func (clusterUtils ClusterResourceUtilsImpl) createClusterObject(ctx context.Context, + state *ClusterState, diags Diagnostics) (*cmv1.Cluster, error) { // Create the cluster: builder := cmv1.NewCluster() builder.Name(state.Name.Value) @@ -351,68 +342,79 @@ func createClusterObject(ctx context.Context, return object, err } -func (r *ClusterResource) Create(ctx context.Context, - request tfsdk.CreateResourceRequest, response *tfsdk.CreateResourceResponse) { - // Get the plan: - state := &ClusterState{} - diags := request.Plan.Get(ctx, state) - response.Diagnostics.Append(diags...) - if response.Diagnostics.HasError() { - return - } +type Diagnostics interface { + AddError(title, description string) +} + +func (clusterUtils ClusterResourceUtilsImpl) create(ctx context.Context, + state *ClusterState, diags Diagnostics) error { - object, err := createClusterObject(ctx, state, diags) + object, err := clusterUtils.createClusterObject(ctx, state, diags) if err != nil { - response.Diagnostics.AddError( + diags.AddError( "Can't build cluster", fmt.Sprintf( "Can't build cluster with name '%s': %v", state.Name.Value, err, ), ) - return + return err } - add, err := r.collection.Add().Body(object).SendContext(ctx) + object, err = clusterUtils.clusterClient.Create(ctx, object) if err != nil { - response.Diagnostics.AddError( - "Can't create cluster", + diags.AddError( + clusterCreationFailureMessage, fmt.Sprintf( "Can't create cluster with name '%s': %v", state.Name.Value, err, ), ) - return + return err } - object = add.Body() // Wait till the cluster is ready unless explicitly disabled: wait := state.Wait.Unknown || state.Wait.Null || state.Wait.Value ready := object.State() == cmv1.ClusterStateReady if wait && !ready { - pollCtx, cancel := context.WithTimeout(ctx, 1*time.Hour) - defer cancel() - _, err := r.collection.Cluster(object.ID()).Poll(). - Interval(30 * time.Second). - Predicate(func(get *cmv1.ClusterGetResponse) bool { - object = get.Body() - return object.State() == cmv1.ClusterStateReady - }). - StartContext(pollCtx) + err := clusterUtils.clusterClient.PollReady(ctx, object.ID()) if err != nil { - response.Diagnostics.AddError( - "Can't poll cluster state", + diags.AddError( + clusterPollFailure, fmt.Sprintf( "Can't poll state of cluster with identifier '%s': %v", object.ID(), err, ), ) - return + return err } } // Save the state: - populateClusterState(object, state) + clusterUtils.populateClusterState(object, state) + + return nil +} + +func (r *ClusterResource) Create(ctx context.Context, + request tfsdk.CreateResourceRequest, response *tfsdk.CreateResourceResponse) { + // Get the plan: + state := &ClusterState{} + diags := request.Plan.Get(ctx, state) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + clusterClient := &ClusterClientImpl{InternalClient: r.collection} + + clusterUtils := ClusterResourceUtilsImpl{clusterClient: clusterClient} + + err := clusterUtils.create(ctx, state, &diags) + if err != nil { + return + } + diags = response.State.Set(ctx, state) response.Diagnostics.Append(diags...) } @@ -427,6 +429,9 @@ func (r *ClusterResource) Read(ctx context.Context, request tfsdk.ReadResourceRe return } + clusterClient := &ClusterClientImpl{InternalClient: r.collection} + clusterUtils := ClusterResourceUtilsImpl{clusterClient: clusterClient} + // Find the cluster: get, err := r.collection.Cluster(state.ID.Value).Get().SendContext(ctx) if err != nil { @@ -442,35 +447,17 @@ func (r *ClusterResource) Read(ctx context.Context, request tfsdk.ReadResourceRe object := get.Body() // Save the state: - populateClusterState(object, state) + clusterUtils.populateClusterState(object, state) diags = response.State.Set(ctx, state) response.Diagnostics.Append(diags...) } -func (r *ClusterResource) Update(ctx context.Context, request tfsdk.UpdateResourceRequest, - response *tfsdk.UpdateResourceResponse) { - var diags diag.Diagnostics - - // Get the state: - state := &ClusterState{} - diags = request.State.Get(ctx, state) - response.Diagnostics.Append(diags...) - if response.Diagnostics.HasError() { - return - } - - // Get the plan: - plan := &ClusterState{} - diags = request.Plan.Get(ctx, plan) - response.Diagnostics.Append(diags...) - if response.Diagnostics.HasError() { - return - } +func (clusterUtils ClusterResourceUtilsImpl) update(ctx context.Context, + currentState *ClusterState, desiredState *ClusterState, diags Diagnostics) error { - // Send request to update the cluster: builder := cmv1.NewCluster() - var nodes *cmv1.ClusterNodesBuilder - compute, ok := shouldPatchInt(state.ComputeNodes, plan.ComputeNodes) + nodes := cmv1.NewClusterNodes() + compute, ok := shouldPatchInt(currentState.ComputeNodes, desiredState.ComputeNodes) if ok { nodes.Compute(int(compute)) } @@ -479,84 +466,116 @@ func (r *ClusterResource) Update(ctx context.Context, request tfsdk.UpdateResour } patch, err := builder.Build() if err != nil { - response.Diagnostics.AddError( + diags.AddError( "Can't build cluster patch", fmt.Sprintf( "Can't build patch for cluster with identifier '%s': %v", - state.ID.Value, err, + currentState.ID.Value, err, ), ) - return + return err } - update, err := r.collection.Cluster(state.ID.Value).Update(). - Body(patch). - SendContext(ctx) + object, err := clusterUtils.clusterClient.Update(ctx, currentState.ID.Value, patch) if err != nil { - response.Diagnostics.AddError( - "Can't update cluster", + diags.AddError( + clusterUpdateFailureMessage, fmt.Sprintf( "Can't update cluster with identifier '%s': %v", - state.ID.Value, err, + currentState.ID.Value, err, ), ) - return + return err } - object := update.Body() // Update the state: - populateClusterState(object, state) - diags = response.State.Set(ctx, state) - response.Diagnostics.Append(diags...) + clusterUtils.populateClusterState(object, currentState) + + return nil } -func (r *ClusterResource) Delete(ctx context.Context, request tfsdk.DeleteResourceRequest, - response *tfsdk.DeleteResourceResponse) { +func (r *ClusterResource) Update(ctx context.Context, request tfsdk.UpdateResourceRequest, + response *tfsdk.UpdateResourceResponse) { + var diags diag.Diagnostics + // Get the state: state := &ClusterState{} - diags := request.State.Get(ctx, state) + diags = request.State.Get(ctx, state) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + // Get the plan: + plan := &ClusterState{} + diags = request.Plan.Get(ctx, plan) response.Diagnostics.Append(diags...) if response.Diagnostics.HasError() { return } - // Send the request to delete the cluster: - resource := r.collection.Cluster(state.ID.Value) - _, err := resource.Delete().SendContext(ctx) + clusterClient := &ClusterClientImpl{InternalClient: r.collection} + + clusterUtils := ClusterResourceUtilsImpl{clusterClient: clusterClient} + err := clusterUtils.update(ctx, state, plan, &diags) if err != nil { - response.Diagnostics.AddError( - "Can't delete cluster", + return + } + + diags = response.State.Set(ctx, state) + response.Diagnostics.Append(diags...) +} + +func (clusterUtils ClusterResourceUtilsImpl) delete(ctx context.Context, + id string, shouldPoll bool, diags Diagnostics) error { + err := clusterUtils.clusterClient.Delete(ctx, id) + if err != nil { + diags.AddError( + clusterDeleteFailureMessage, fmt.Sprintf( "Can't delete cluster with identifier '%s': %v", - state.ID.Value, err, + id, err, ), ) - return + return err } // Wait till the cluster has been effectively deleted: - if state.Wait.Unknown || state.Wait.Null || state.Wait.Value { - pollCtx, cancel := context.WithTimeout(ctx, 10*time.Minute) - defer cancel() - _, err := resource.Poll(). - Interval(30 * time.Second). - Status(http.StatusNotFound). - StartContext(pollCtx) - sdkErr, ok := err.(*errors.Error) - if ok && sdkErr.Status() == http.StatusNotFound { - err = nil - } + if shouldPoll { + err := clusterUtils.clusterClient.PollRemoved(ctx, id) if err != nil { - response.Diagnostics.AddError( - "Can't poll cluster deletion", + diags.AddError( + clusterPollDeletionFailure, fmt.Sprintf( "Can't poll deletion of cluster with identifier '%s': %v", - state.ID.Value, err, + id, err, ), ) - return + return err } } + return nil +} + +func (r *ClusterResource) Delete(ctx context.Context, request tfsdk.DeleteResourceRequest, + response *tfsdk.DeleteResourceResponse) { + // Get the state: + state := &ClusterState{} + diags := request.State.Get(ctx, state) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + clusterClient := &ClusterClientImpl{InternalClient: r.collection} + + clusterUtils := ClusterResourceUtilsImpl{clusterClient: clusterClient} + shouldPoll := state.Wait.Unknown || state.Wait.Null || state.Wait.Value + err := clusterUtils.delete(ctx, state.ID.Value, shouldPoll, &diags) + if err != nil { + return + } + // Remove the state: response.State.RemoveResource(ctx) } @@ -577,15 +596,17 @@ func (r *ClusterResource) ImportState(ctx context.Context, request tfsdk.ImportR } object := get.Body() + clusterUtils := ClusterResourceUtilsImpl{} + // Save the state: state := &ClusterState{} - populateClusterState(object, state) + clusterUtils.populateClusterState(object, state) diags := response.State.Set(ctx, state) response.Diagnostics.Append(diags...) } // populateClusterState copies the data from the API object to the Terraform state. -func populateClusterState(object *cmv1.Cluster, state *ClusterState) { +func (clusterUtils ClusterResourceUtilsImpl) populateClusterState(object *cmv1.Cluster, state *ClusterState) { state.ID = types.String{ Value: object.ID(), } diff --git a/provider/cluster_resource_common.go b/provider/cluster_resource_common.go new file mode 100644 index 0000000..a4c2392 --- /dev/null +++ b/provider/cluster_resource_common.go @@ -0,0 +1,132 @@ +/* +Copyright (c) 2021 Red Hat, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import ( + "context" + "net/http" + "time" + + "github.com/hashicorp/terraform-plugin-framework/diag" + cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1" + "github.com/openshift-online/ocm-sdk-go/errors" + "github.com/openshift-online/ocm-sdk-go/logging" +) + +const ( + clusterCreationFailureMessage = "Can't create cluster" + clusterPollFailure = "Can't poll cluster state" + clusterUpdateFailureMessage = "Can't update cluster" + clusterDeleteFailureMessage = "Can't delete cluster" + clusterPollDeletionFailure = "Can't poll cluster deletion" +) + +type ClusterResourceType struct { +} + +type ClusterResource struct { + logger logging.Logger + collection *cmv1.ClustersClient +} + +type ClusterClient interface { + Get(ctx context.Context, id string) (*cmv1.Cluster, error) + Create(ctx context.Context, cluster *cmv1.Cluster) (*cmv1.Cluster, error) + PollReady(ctx context.Context, id string) error + PollRemoved(ctx context.Context, id string) error + Update(ctx context.Context, id string, cluster *cmv1.Cluster) (*cmv1.Cluster, error) + Delete(ctx context.Context, id string) error +} + +type ClusterClientImpl struct { + InternalClient *cmv1.ClustersClient +} + +func (clusterClient *ClusterClientImpl) Get(ctx context.Context, id string) (*cmv1.Cluster, error) { + response, err := clusterClient.InternalClient.Cluster(id).Get().SendContext(ctx) + if err != nil { + return nil, err + } + return response.Body(), nil +} + +func (clusterClient *ClusterClientImpl) Create(ctx context.Context, cluster *cmv1.Cluster) (*cmv1.Cluster, error) { + response, err := clusterClient.InternalClient.Add().Body(cluster).SendContext(ctx) + if err != nil { + return nil, err + } + return response.Body(), nil +} + +func (clusterClient *ClusterClientImpl) PollReady(ctx context.Context, id string) error { + pollCtx, cancel := context.WithTimeout(ctx, 1*time.Hour) + defer cancel() + _, err := clusterClient.InternalClient.Cluster(id).Poll(). + Interval(30 * time.Second). + Predicate(func(get *cmv1.ClusterGetResponse) bool { + object := get.Body() + return object.State() == cmv1.ClusterStateReady + }). + StartContext(pollCtx) + if err != nil { + return err + } + + return nil +} + +func (clusterClient *ClusterClientImpl) PollRemoved(ctx context.Context, id string) error { + pollCtx, cancel := context.WithTimeout(ctx, 10*time.Minute) + defer cancel() + _, err := clusterClient.InternalClient.Cluster(id).Poll(). + Interval(30 * time.Second). + Status(http.StatusNotFound). + StartContext(pollCtx) + sdkErr, ok := err.(*errors.Error) + if ok && sdkErr.Status() == http.StatusNotFound { + err = nil + } + if err != nil { + return err + } + + return nil +} + +func (clusterClient *ClusterClientImpl) Update(ctx context.Context, id string, patch *cmv1.Cluster) (*cmv1.Cluster, error) { + update, err := clusterClient.InternalClient.Cluster(id).Update(). + Body(patch). + SendContext(ctx) + if err != nil { + return nil, err + } + return update.Body(), nil +} + +func (clusterClient *ClusterClientImpl) Delete(ctx context.Context, id string) error { + _, err := clusterClient.InternalClient.Cluster(id).Delete().SendContext(ctx) + if err != nil { + return err + } + return nil +} + +type ClusterResourceUtils interface { + populateClusterState(object *cmv1.Cluster, state *ClusterState) + createClusterObject(ctx context.Context, + state *ClusterState, diags diag.Diagnostics) (*cmv1.Cluster, error) +} \ No newline at end of file diff --git a/provider/cluster_resource_mock.go b/provider/cluster_resource_mock.go new file mode 100644 index 0000000..78a2768 --- /dev/null +++ b/provider/cluster_resource_mock.go @@ -0,0 +1,209 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: provider/cluster_resource.go + +// Package provider is a generated GoMock package. +package provider + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + diag "github.com/hashicorp/terraform-plugin-framework/diag" + v1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1" +) + +// MockClusterClient is a mock of ClusterClient interface. +type MockClusterClient struct { + ctrl *gomock.Controller + recorder *MockClusterClientMockRecorder +} + +// MockClusterClientMockRecorder is the mock recorder for MockClusterClient. +type MockClusterClientMockRecorder struct { + mock *MockClusterClient +} + +// NewMockClusterClient creates a new mock instance. +func NewMockClusterClient(ctrl *gomock.Controller) *MockClusterClient { + mock := &MockClusterClient{ctrl: ctrl} + mock.recorder = &MockClusterClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockClusterClient) EXPECT() *MockClusterClientMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockClusterClient) Create(ctx context.Context, cluster *v1.Cluster) (*v1.Cluster, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", ctx, cluster) + ret0, _ := ret[0].(*v1.Cluster) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Create indicates an expected call of Create. +func (mr *MockClusterClientMockRecorder) Create(ctx, cluster interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockClusterClient)(nil).Create), ctx, cluster) +} + +// Delete mocks base method. +func (m *MockClusterClient) Delete(ctx context.Context, id string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", ctx, id) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockClusterClientMockRecorder) Delete(ctx, id interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockClusterClient)(nil).Delete), ctx, id) +} + +// Get mocks base method. +func (m *MockClusterClient) Get(ctx context.Context, id string) (*v1.Cluster, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, id) + ret0, _ := ret[0].(*v1.Cluster) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockClusterClientMockRecorder) Get(ctx, id interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockClusterClient)(nil).Get), ctx, id) +} + +// PollReady mocks base method. +func (m *MockClusterClient) PollReady(ctx context.Context, id string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PollReady", ctx, id) + ret0, _ := ret[0].(error) + return ret0 +} + +// PollReady indicates an expected call of PollReady. +func (mr *MockClusterClientMockRecorder) PollReady(ctx, id interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PollReady", reflect.TypeOf((*MockClusterClient)(nil).PollReady), ctx, id) +} + +// PollRemoved mocks base method. +func (m *MockClusterClient) PollRemoved(ctx context.Context, id string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PollRemoved", ctx, id) + ret0, _ := ret[0].(error) + return ret0 +} + +// PollRemoved indicates an expected call of PollRemoved. +func (mr *MockClusterClientMockRecorder) PollRemoved(ctx, id interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PollRemoved", reflect.TypeOf((*MockClusterClient)(nil).PollRemoved), ctx, id) +} + +// Update mocks base method. +func (m *MockClusterClient) Update(ctx context.Context, id string, cluster *v1.Cluster) (*v1.Cluster, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", ctx, id, cluster) + ret0, _ := ret[0].(*v1.Cluster) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update. +func (mr *MockClusterClientMockRecorder) Update(ctx, id, cluster interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockClusterClient)(nil).Update), ctx, id, cluster) +} + +// MockClusterResourceUtils is a mock of ClusterResourceUtils interface. +type MockClusterResourceUtils struct { + ctrl *gomock.Controller + recorder *MockClusterResourceUtilsMockRecorder +} + +// MockClusterResourceUtilsMockRecorder is the mock recorder for MockClusterResourceUtils. +type MockClusterResourceUtilsMockRecorder struct { + mock *MockClusterResourceUtils +} + +// NewMockClusterResourceUtils creates a new mock instance. +func NewMockClusterResourceUtils(ctrl *gomock.Controller) *MockClusterResourceUtils { + mock := &MockClusterResourceUtils{ctrl: ctrl} + mock.recorder = &MockClusterResourceUtilsMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockClusterResourceUtils) EXPECT() *MockClusterResourceUtilsMockRecorder { + return m.recorder +} + +// createClusterObject mocks base method. +func (m *MockClusterResourceUtils) createClusterObject(ctx context.Context, state *ClusterState, diags diag.Diagnostics) (*v1.Cluster, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "createClusterObject", ctx, state, diags) + ret0, _ := ret[0].(*v1.Cluster) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// createClusterObject indicates an expected call of createClusterObject. +func (mr *MockClusterResourceUtilsMockRecorder) createClusterObject(ctx, state, diags interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "createClusterObject", reflect.TypeOf((*MockClusterResourceUtils)(nil).createClusterObject), ctx, state, diags) +} + +// populateClusterState mocks base method. +func (m *MockClusterResourceUtils) populateClusterState(object *v1.Cluster, state *ClusterState) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "populateClusterState", object, state) +} + +// populateClusterState indicates an expected call of populateClusterState. +func (mr *MockClusterResourceUtilsMockRecorder) populateClusterState(object, state interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "populateClusterState", reflect.TypeOf((*MockClusterResourceUtils)(nil).populateClusterState), object, state) +} + +// MockDiagnostics is a mock of Diagnostics interface. +type MockDiagnostics struct { + ctrl *gomock.Controller + recorder *MockDiagnosticsMockRecorder +} + +// MockDiagnosticsMockRecorder is the mock recorder for MockDiagnostics. +type MockDiagnosticsMockRecorder struct { + mock *MockDiagnostics +} + +// NewMockDiagnostics creates a new mock instance. +func NewMockDiagnostics(ctrl *gomock.Controller) *MockDiagnostics { + mock := &MockDiagnostics{ctrl: ctrl} + mock.recorder = &MockDiagnosticsMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDiagnostics) EXPECT() *MockDiagnosticsMockRecorder { + return m.recorder +} + +// AddError mocks base method. +func (m *MockDiagnostics) AddError(title, description string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "AddError", title, description) +} + +// AddError indicates an expected call of AddError. +func (mr *MockDiagnosticsMockRecorder) AddError(title, description interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddError", reflect.TypeOf((*MockDiagnostics)(nil).AddError), title, description) +} diff --git a/provider/cluster_resource_test.go b/provider/cluster_resource_test.go index 18b15af..2e7d408 100644 --- a/provider/cluster_resource_test.go +++ b/provider/cluster_resource_test.go @@ -19,39 +19,116 @@ package provider import ( "context" "encoding/json" + "fmt" + gomock "github.com/golang/mock/gomock" "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types" . "github.com/onsi/ginkgo/v2/dsl/core" // nolint . "github.com/onsi/gomega" // nolint cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1" ) +type GinkgoTestReporter struct{} + +func (g GinkgoTestReporter) Errorf(format string, args ...interface{}) { + Fail(fmt.Sprintf(format, args...)) +} + +func (g GinkgoTestReporter) Fatalf(format string, args ...interface{}) { + Fail(fmt.Sprintf(format, args...)) +} + +type Matcher struct { + comparator func(x interface{}) bool + text string +} + +func (matcher Matcher) Matches(x interface{}) bool { + return matcher.comparator(x) +} + +func (matcher Matcher) String() string { + return matcher.text +} + +func generateBaseClusterMap() map[string]interface{} { + return map[string]interface{}{ + "id": clusterId, + "product": map[string]interface{}{ + "id": productId, + }, + "cloud_provider": map[string]interface{}{ + "id": cloudProviderId, + }, + "region": map[string]interface{}{ + "id": regionId, + }, + "multi_az": multiAz, + "properties": map[string]interface{}{ + "rosa_creator_arn": rosaCreatorArn, + }, + "api": map[string]interface{}{ + "url": apiUrl, + }, + "console": map[string]interface{}{ + "url": consoleUrl, + }, + "nodes": map[string]interface{}{ + "compute_machine_type": map[string]interface{}{ + "id": machineType, + }, + "availability_zones": []interface{}{ + availabilityZone1, + }, + "compute": computeNodes, + }, + "ccs": map[string]interface{}{ + "enabled": ccsEnabled, + }, + "aws": map[string]interface{}{ + "account_id": awsAccountID, + "access_key_id": awsAccessKeyID, + "secret_access_key": awsSecretAccessKey, + "private_link": privateLink, + }, + "version": map[string]interface{}{ + "id": clusterVersion, + }, + } +} + var _ = Describe("Cluster creation", func() { - clusterId := "1n2j3k4l5m6n7o8p9q0r" - clusterName := "my-cluster" - clusterVersion := "openshift-v4.11.12" - productId := "rosa" - cloudProviderId := "aws" - regionId := "us-east-1" - multiAz := true - rosaCreatorArn := "arn:aws:iam::123456789012:dummy/dummy" - apiUrl := "https://api.my-cluster.com:6443" - consoleUrl := "https://console.my-cluster.com" - machineType := "m5.xlarge" - availabilityZone := "us-east-1a" - ccsEnabled := true - awsAccountID := "123456789012" - awsAccessKeyID := "AKIAIOSFODNN7EXAMPLE" - awsSecretAccessKey := "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" - privateLink := false + - It("Creates ClusterBuilder with correct field values", func() { + parseClusterMapToObject := func(clusterMap map[string]interface{}) *cmv1.Cluster { + clusterJsonString, err := json.Marshal(clusterMap) + Expect(err).To(BeNil()) + + clusterObject, err := cmv1.UnmarshalCluster(clusterJsonString) + Expect(err).To(BeNil()) + + return clusterObject + } + + generateBaseClusterObject := func() *cmv1.Cluster { + clusterJson := generateBaseClusterMap() + clusterObject := parseClusterMapToObject(clusterJson) + + return clusterObject + } + + generateBaseClusterState := func() *ClusterState { clusterState := &ClusterState{ + ID: types.String{ + Value: clusterId, + }, Name: types.String{ Value: clusterName, }, + ComputeNodes: types.Int64{ + Value: int64(computeNodes), + }, Version: types.String{ Value: clusterVersion, }, @@ -64,7 +141,7 @@ var _ = Describe("Cluster creation", func() { AvailabilityZones: types.List{ Elems: []attr.Value{ types.String{ - Value: availabilityZone, + Value: availabilityZone1, }, }, }, @@ -79,7 +156,22 @@ var _ = Describe("Cluster creation", func() { Value: false, }, } - clusterObject, err := createClusterObject(context.Background(), clusterState, diag.Diagnostics{}) + + return clusterState + } + + generateGinkgoController := func() *gomock.Controller { + reporter := GinkgoTestReporter{} + controller := gomock.NewController(reporter) + return controller + } + + It("Creates ClusterBuilder with correct field values", func() { + ctrl := generateGinkgoController() + clusterState := generateBaseClusterState() + + clusterUtils := ClusterResourceUtilsImpl{} + clusterObject, err := clusterUtils.createClusterObject(context.Background(), clusterState, NewMockDiagnostics(ctrl)) Expect(err).To(BeNil()) Expect(err).To(BeNil()) @@ -96,7 +188,7 @@ var _ = Describe("Cluster creation", func() { availabilityZones := clusterObject.Nodes().AvailabilityZones() Expect(availabilityZones).To(HaveLen(1)) - Expect(availabilityZones[0]).To(Equal(availabilityZone)) + Expect(availabilityZones[0]).To(Equal(availabilityZone1)) arn, ok := clusterObject.Properties()["rosa_creator_arn"] Expect(ok).To(BeTrue()) @@ -105,57 +197,12 @@ var _ = Describe("Cluster creation", func() { It("populateClusterState converts correctly a Cluster object into a ClusterState", func() { // We builder a Cluster object by creating a json and using cmv1.UnmarshalCluster on it - clusterJson := map[string]interface{}{ - "id": clusterId, - "product": map[string]interface{}{ - "id": productId, - }, - "cloud_provider": map[string]interface{}{ - "id": cloudProviderId, - }, - "region": map[string]interface{}{ - "id": regionId, - }, - "multi_az": multiAz, - "properties": map[string]interface{}{ - "rosa_creator_arn": rosaCreatorArn, - }, - "api": map[string]interface{}{ - "url": apiUrl, - }, - "console": map[string]interface{}{ - "url": consoleUrl, - }, - "nodes": map[string]interface{}{ - "compute_machine_type": map[string]interface{}{ - "id": machineType, - }, - "availability_zones": []interface{}{ - availabilityZone, - }, - }, - "ccs": map[string]interface{}{ - "enabled": ccsEnabled, - }, - "aws": map[string]interface{}{ - "account_id": awsAccountID, - "access_key_id": awsAccessKeyID, - "secret_access_key": awsSecretAccessKey, - "private_link": privateLink, - }, - "version": map[string]interface{}{ - "id": clusterVersion, - }, - } - clusterJsonString, err := json.Marshal(clusterJson) - Expect(err).To(BeNil()) - - clusterObject, err := cmv1.UnmarshalCluster(clusterJsonString) - Expect(err).To(BeNil()) + clusterObject := generateBaseClusterObject() //We convert the Cluster object into a ClusterState and check that the conversion is correct clusterState := &ClusterState{} - populateClusterState(clusterObject, clusterState) + clusterUtils := ClusterResourceUtilsImpl{} + clusterUtils.populateClusterState(clusterObject, clusterState) Expect(clusterState.ID.Value).To(Equal(clusterId)) Expect(clusterState.Version.Value).To(Equal(clusterVersion)) @@ -168,11 +215,249 @@ var _ = Describe("Cluster creation", func() { Expect(clusterState.ConsoleURL.Value).To(Equal(consoleUrl)) Expect(clusterState.ComputeMachineType.Value).To(Equal(machineType)) Expect(clusterState.AvailabilityZones.Elems).To(HaveLen(1)) - Expect(clusterState.AvailabilityZones.Elems[0].Equal(types.String{Value: availabilityZone})).To(Equal(true)) + Expect(clusterState.AvailabilityZones.Elems[0].Equal(types.String{Value: availabilityZone1})).To(Equal(true)) Expect(clusterState.CCSEnabled.Value).To(Equal(ccsEnabled)) Expect(clusterState.AWSAccountID.Value).To(Equal(awsAccountID)) Expect(clusterState.AWSAccessKeyID.Value).To(Equal(awsAccessKeyID)) Expect(clusterState.AWSSecretAccessKey.Value).To(Equal(awsSecretAccessKey)) Expect(clusterState.AWSPrivateLink.Value).To(Equal(privateLink)) }) + + Describe("Test the create function of the default clusterUtils", func() { + var ( + ctrl *gomock.Controller + clusterState *ClusterState + clusterMap map[string]interface{} + clusterObject *cmv1.Cluster + mockDiags *MockDiagnostics + mockClient *MockClusterClient + + ) + BeforeEach(func() { + ctrl = generateGinkgoController() + clusterState = generateBaseClusterState() + clusterMap = generateBaseClusterMap() + clusterObject = parseClusterMapToObject(clusterMap) + mockDiags = NewMockDiagnostics(ctrl) + mockClient = NewMockClusterClient(ctrl) + }) + + It("invokes cluster creation on the client", func() { + comparator := func(x interface{}) bool { + clusterObject := x.(*cmv1.Cluster) + return clusterObject.Name() == clusterName + } + matcher := Matcher{ + comparator: comparator, + text: "cluster name is correct", + } + + mockClient.EXPECT().Create(context.Background(), matcher).Return(clusterObject, nil) + + clusterUtils := ClusterResourceUtilsImpl{ + clusterClient: mockClient, + } + + err := clusterUtils.create(context.Background(), clusterState, mockDiags) + Expect(err).To(BeNil()) + }) + + It("reports clusterCreationFailureMessage on client creation error", func() { + mockDiags.EXPECT().AddError(clusterCreationFailureMessage, gomock.Any()) + + comparator := func(x interface{}) bool { + clusterObject := x.(*cmv1.Cluster) + return clusterObject.Name() == clusterName + } + matcher := Matcher{ + comparator: comparator, + text: "cluster name is correct", + } + + mockClient.EXPECT().Create(context.Background(), matcher).Return(clusterObject, fmt.Errorf("error")) + + clusterUtils := ClusterResourceUtilsImpl{ + clusterClient: mockClient, + } + + err := clusterUtils.create(context.Background(), clusterState, mockDiags) + Expect(err).ToNot(BeNil()) + }) + + It("reports clusterPollFailure on client polling error", func() { + clusterState.Wait.Value = true + + mockDiags.EXPECT().AddError(clusterPollFailure, gomock.Any()) + + comparator := func(x interface{}) bool { + clusterObject := x.(*cmv1.Cluster) + return clusterObject.Name() == clusterName + } + matcher := Matcher{ + comparator: comparator, + text: "cluster name is correct", + } + + mockClient.EXPECT().Create(context.Background(), matcher).Return(clusterObject, nil) + mockClient.EXPECT().PollReady(gomock.Any(), gomock.Any()).Return(fmt.Errorf("error")) + + clusterUtils := ClusterResourceUtilsImpl{ + clusterClient: mockClient, + } + + err := clusterUtils.create(context.Background(), clusterState, mockDiags) + Expect(err).ToNot(BeNil()) + }) + }) + + Describe("Test the update function of the default clusterUtils", func() { + var ( + ctrl *gomock.Controller + clusterCurrentState *ClusterState + clusterDesiredState *ClusterState + clusterMap map[string]interface{} + clusterObject *cmv1.Cluster + mockDiags *MockDiagnostics + mockClient *MockClusterClient + + ) + BeforeEach(func() { + ctrl = generateGinkgoController() + clusterCurrentState = generateBaseClusterState() + clusterDesiredState = generateBaseClusterState() + clusterMap = generateBaseClusterMap() + clusterObject = parseClusterMapToObject(clusterMap) + mockDiags = NewMockDiagnostics(ctrl) + mockClient = NewMockClusterClient(ctrl) + }) + + It("invokes cluster update on the client", func() { + clusterDesiredState.ComputeNodes.Value = clusterDesiredState.ComputeNodes.Value + 1 + clusterMap["nodes"].(map[string]interface{})["compute"] = clusterDesiredState.ComputeNodes.Value + + comparator := func(x interface{}) bool { + clusterObject := x.(*cmv1.Cluster) + return clusterObject.Nodes().Compute() == int(clusterDesiredState.ComputeNodes.Value) + } + matcher := Matcher{ + comparator: comparator, + text: "compute node conut is updated correctly", + } + + + mockClient.EXPECT().Update(context.Background(), clusterId, matcher).Return(clusterObject, nil) + + clusterUtils := ClusterResourceUtilsImpl{ + clusterClient: mockClient, + } + + err := clusterUtils.update(context.Background(), clusterCurrentState, clusterDesiredState, mockDiags) + Expect(err).To(BeNil()) + }) + + It("reports clusterUpdateFailureMessage on client update error", func() { + clusterDesiredState.ComputeNodes.Value = clusterDesiredState.ComputeNodes.Value + 1 + clusterMap["nodes"].(map[string]interface{})["compute"] = clusterDesiredState.ComputeNodes.Value + + mockDiags.EXPECT().AddError(clusterUpdateFailureMessage, gomock.Any()) + + comparator := func(x interface{}) bool { + clusterObject := x.(*cmv1.Cluster) + return clusterObject.Nodes().Compute() == int(clusterDesiredState.ComputeNodes.Value) + } + matcher := Matcher{ + comparator: comparator, + text: "compute node conut is updated correctly", + } + + mockClient.EXPECT().Update(context.Background(), clusterId, matcher).Return(clusterObject, fmt.Errorf("error")) + + clusterUtils := ClusterResourceUtilsImpl{ + clusterClient: mockClient, + } + + err := clusterUtils.update(context.Background(), clusterCurrentState, clusterDesiredState, mockDiags) + Expect(err).ToNot(BeNil()) + }) + + It("reports clusterUpdateFailureMessage on client update error", func() { + clusterDesiredState.ComputeNodes.Value = clusterDesiredState.ComputeNodes.Value + 1 + clusterMap["nodes"].(map[string]interface{})["compute"] = clusterDesiredState.ComputeNodes.Value + + mockDiags.EXPECT().AddError(clusterUpdateFailureMessage, gomock.Any()) + + comparator := func(x interface{}) bool { + clusterObject := x.(*cmv1.Cluster) + return clusterObject.Nodes().Compute() == int(clusterDesiredState.ComputeNodes.Value) + } + matcher := Matcher{ + comparator: comparator, + text: "compute node conut is updated correctly", + } + + mockClient.EXPECT().Update(context.Background(), clusterId, matcher).Return(clusterObject, fmt.Errorf("error")) + + clusterUtils := ClusterResourceUtilsImpl{ + clusterClient: mockClient, + } + + err := clusterUtils.update(context.Background(), clusterCurrentState, clusterDesiredState, mockDiags) + Expect(err).ToNot(BeNil()) + }) + }) + + Describe("Test the delete function of the default clusterUtils", func() { + var ( + ctrl *gomock.Controller + mockDiags *MockDiagnostics + mockClient *MockClusterClient + + ) + BeforeEach(func() { + ctrl = generateGinkgoController() + mockDiags = NewMockDiagnostics(ctrl) + mockClient = NewMockClusterClient(ctrl) + }) + + It("invokes cluster deletion on the client", func() { + mockClient.EXPECT().Delete(gomock.Any(), gomock.Eq(clusterId)).Return(nil) + mockClient.EXPECT().PollRemoved(gomock.Any(), gomock.Eq(clusterId)).Return(nil) + + clusterUtils := ClusterResourceUtilsImpl{ + clusterClient: mockClient, + } + + err := clusterUtils.delete(context.Background(), clusterId, true, mockDiags) + Expect(err).To(BeNil()) + }) + + It("reports clusterDeleteFailureMessage on client delete error", func() { + mockDiags.EXPECT().AddError(clusterDeleteFailureMessage, gomock.Any()) + + mockClient.EXPECT().Delete(gomock.Any(), gomock.Eq(clusterId)).Return(fmt.Errorf("error")) + mockClient.EXPECT().PollRemoved(gomock.Any(), gomock.Eq(clusterId)).Return(nil) + + clusterUtils := ClusterResourceUtilsImpl{ + clusterClient: mockClient, + } + + err := clusterUtils.delete(context.Background(), clusterId, true, mockDiags) + Expect(err).ToNot(BeNil()) + }) + + It("reports clusterPollDeletionFailure on client deletion polling error", func() { + mockDiags.EXPECT().AddError(clusterPollDeletionFailure, gomock.Any()) + + mockClient.EXPECT().Delete(gomock.Any(), gomock.Eq(clusterId)).Return(nil) + mockClient.EXPECT().PollRemoved(gomock.Any(), gomock.Eq(clusterId)).Return(fmt.Errorf("error")) + + clusterUtils := ClusterResourceUtilsImpl{ + clusterClient: mockClient, + } + + err := clusterUtils.delete(context.Background(), clusterId, true, mockDiags) + Expect(err).ToNot(BeNil()) + }) + }) + }) diff --git a/provider/cluster_resource_test_common.go b/provider/cluster_resource_test_common.go new file mode 100644 index 0000000..796750a --- /dev/null +++ b/provider/cluster_resource_test_common.go @@ -0,0 +1,43 @@ +/* +Copyright (c) 2021 Red Hat, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +const ( + clusterId = "1n2j3k4l5m6n7o8p9q0r" + clusterName = "my-cluster" + clusterVersion = "openshift-v4.11.12" + productId = "rosa" + cloudProviderId = "aws" + regionId = "us-east-1" + multiAz = true + rosaCreatorArn = "arn:aws:iam::123456789012:dummy/dummy" + apiUrl = "https://api.my-cluster.com:6443" + consoleUrl = "https://console.my-cluster.com" + machineType = "m5.xlarge" + availabilityZone1 = "us-east-1a" + availabilityZone2 = "us-east-1b" + ccsEnabled = true + awsAccountID = "123456789012" + awsAccessKeyID = "AKIAIOSFODNN7EXAMPLE" + awsSecretAccessKey = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" + privateLink = false + computeNodes = 3 + oidcEndpointUrl = "example.com" + roleArn = "arn:aws:iam::123456789012:role/role-name" + httpProxy = "http://proxy.com" + httpsProxy = "https://proxy.com" +) diff --git a/provider/cluster_rosa_classic_resource_test.go b/provider/cluster_rosa_classic_resource_test.go index 87e5cb2..34e3f7f 100644 --- a/provider/cluster_rosa_classic_resource_test.go +++ b/provider/cluster_rosa_classic_resource_test.go @@ -40,26 +40,6 @@ func (c MockHttpClient) Get(url string) (resp *http.Response, err error) { return c.response, nil } -const ( - clusterId = "1n2j3k4l5m6n7o8p9q0r" - clusterName = "my-cluster" - regionId = "us-east-1" - multiAz = true - rosaCreatorArn = "arn:aws:iam::123456789012:dummy/dummy" - apiUrl = "https://api.my-cluster.com:6443" - consoleUrl = "https://console.my-cluster.com" - machineType = "m5.xlarge" - availabilityZone1 = "us-east-1a" - availabilityZone2 = "us-east-1b" - ccsEnabled = true - awsAccountID = "123456789012" - privateLink = false - oidcEndpointUrl = "example.com" - roleArn = "arn:aws:iam::123456789012:role/role-name" - httpProxy = "http://proxy.com" - httpsProxy = "https://proxy.com" -) - var ( mockHttpClient = MockHttpClient{ response: &http.Response{