diff --git a/common/vpcclient/client/request.go b/common/vpcclient/client/request.go index 618bd3c4..17263f0e 100644 --- a/common/vpcclient/client/request.go +++ b/common/vpcclient/client/request.go @@ -126,6 +126,12 @@ func (r *Request) SetQueryValue(key, value string) *Request { return r } +// SetHeader ... +func (r *Request) SetHeader(key, value string) *Request { + r.headers.Set(key, value) + return r +} + // JSONBody converts the supplied argument to JSON to use as the body of a request func (r *Request) JSONBody(p interface{}) *Request { if r.operation.Method == http.MethodPost && reflect.ValueOf(p).Kind() == reflect.Struct { diff --git a/common/vpcclient/models/share.go b/common/vpcclient/models/share.go index 38e60b9f..d6e6df78 100644 --- a/common/vpcclient/models/share.go +++ b/common/vpcclient/models/share.go @@ -34,6 +34,7 @@ type Share struct { InitialOwner *InitialOwner `json:"initial_owner,omitempty"` Profile *Profile `json:"profile,omitempty"` CreatedAt *time.Time `json:"created_at,omitempty"` + UserTags []string `json:"user_tags,omitempty"` // Status of share named - deleted, deleting, failed, pending, stable, updating, waiting, suspended Status StatusType `json:"lifecycle_state,omitempty"` ShareTargets *[]ShareTarget `json:"mount_targets,omitempty"` diff --git a/common/vpcclient/vpcfilevolume/fakes/share.go b/common/vpcclient/vpcfilevolume/fakes/share.go index b79d0e1a..d2af541d 100644 --- a/common/vpcclient/vpcfilevolume/fakes/share.go +++ b/common/vpcclient/vpcfilevolume/fakes/share.go @@ -109,6 +109,22 @@ type FileShareService struct { result1 *models.Share result2 error } + GetFileShareEtagStub func(string, *zap.Logger) (*models.Share, string, error) + getFileShareEtagMutex sync.RWMutex + getFileShareEtagArgsForCall []struct { + arg1 string + arg2 *zap.Logger + } + getFileShareEtagReturns struct { + result1 *models.Share + result2 string + result3 error + } + getFileShareEtagReturnsOnCall map[int]struct { + result1 *models.Share + result2 string + result3 error + } GetFileShareTargetStub func(string, string, *zap.Logger) (*models.ShareTarget, error) getFileShareTargetMutex sync.RWMutex getFileShareTargetArgsForCall []struct { @@ -202,6 +218,20 @@ type FileShareService struct { result1 *models.SubnetList result2 error } + UpdateFileShareWithEtagStub func(string, string, *models.Share, *zap.Logger) error + updateFileShareWithEtagMutex sync.RWMutex + updateFileShareWithEtagArgsForCall []struct { + arg1 string + arg2 string + arg3 *models.Share + arg4 *zap.Logger + } + updateFileShareWithEtagReturns struct { + result1 error + } + updateFileShareWithEtagReturnsOnCall map[int]struct { + result1 error + } UpdateVolumeStub func(*provider.UpdatePVC, *zap.Logger) error updateVolumeMutex sync.RWMutex updateVolumeArgsForCall []struct { @@ -671,6 +701,74 @@ func (fake *FileShareService) GetFileShareByNameReturnsOnCall(i int, result1 *mo }{result1, result2} } +func (fake *FileShareService) GetFileShareEtag(arg1 string, arg2 *zap.Logger) (*models.Share, string, error) { + fake.getFileShareEtagMutex.Lock() + ret, specificReturn := fake.getFileShareEtagReturnsOnCall[len(fake.getFileShareEtagArgsForCall)] + fake.getFileShareEtagArgsForCall = append(fake.getFileShareEtagArgsForCall, struct { + arg1 string + arg2 *zap.Logger + }{arg1, arg2}) + stub := fake.GetFileShareEtagStub + fakeReturns := fake.getFileShareEtagReturns + fake.recordInvocation("GetFileShareEtag", []interface{}{arg1, arg2}) + fake.getFileShareEtagMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2, ret.result3 + } + return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3 +} + +func (fake *FileShareService) GetFileShareEtagCallCount() int { + fake.getFileShareEtagMutex.RLock() + defer fake.getFileShareEtagMutex.RUnlock() + return len(fake.getFileShareEtagArgsForCall) +} + +func (fake *FileShareService) GetFileShareEtagCalls(stub func(string, *zap.Logger) (*models.Share, string, error)) { + fake.getFileShareEtagMutex.Lock() + defer fake.getFileShareEtagMutex.Unlock() + fake.GetFileShareEtagStub = stub +} + +func (fake *FileShareService) GetFileShareEtagArgsForCall(i int) (string, *zap.Logger) { + fake.getFileShareEtagMutex.RLock() + defer fake.getFileShareEtagMutex.RUnlock() + argsForCall := fake.getFileShareEtagArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FileShareService) GetFileShareEtagReturns(result1 *models.Share, result2 string, result3 error) { + fake.getFileShareEtagMutex.Lock() + defer fake.getFileShareEtagMutex.Unlock() + fake.GetFileShareEtagStub = nil + fake.getFileShareEtagReturns = struct { + result1 *models.Share + result2 string + result3 error + }{result1, result2, result3} +} + +func (fake *FileShareService) GetFileShareEtagReturnsOnCall(i int, result1 *models.Share, result2 string, result3 error) { + fake.getFileShareEtagMutex.Lock() + defer fake.getFileShareEtagMutex.Unlock() + fake.GetFileShareEtagStub = nil + if fake.getFileShareEtagReturnsOnCall == nil { + fake.getFileShareEtagReturnsOnCall = make(map[int]struct { + result1 *models.Share + result2 string + result3 error + }) + } + fake.getFileShareEtagReturnsOnCall[i] = struct { + result1 *models.Share + result2 string + result3 error + }{result1, result2, result3} +} + func (fake *FileShareService) GetFileShareTarget(arg1 string, arg2 string, arg3 *zap.Logger) (*models.ShareTarget, error) { fake.getFileShareTargetMutex.Lock() ret, specificReturn := fake.getFileShareTargetReturnsOnCall[len(fake.getFileShareTargetArgsForCall)] @@ -1070,6 +1168,70 @@ func (fake *FileShareService) ListSubnetsReturnsOnCall(i int, result1 *models.Su }{result1, result2} } +func (fake *FileShareService) UpdateFileShareWithEtag(arg1 string, arg2 string, arg3 *models.Share, arg4 *zap.Logger) error { + fake.updateFileShareWithEtagMutex.Lock() + ret, specificReturn := fake.updateFileShareWithEtagReturnsOnCall[len(fake.updateFileShareWithEtagArgsForCall)] + fake.updateFileShareWithEtagArgsForCall = append(fake.updateFileShareWithEtagArgsForCall, struct { + arg1 string + arg2 string + arg3 *models.Share + arg4 *zap.Logger + }{arg1, arg2, arg3, arg4}) + stub := fake.UpdateFileShareWithEtagStub + fakeReturns := fake.updateFileShareWithEtagReturns + fake.recordInvocation("UpdateFileShareWithEtag", []interface{}{arg1, arg2, arg3, arg4}) + fake.updateFileShareWithEtagMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3, arg4) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FileShareService) UpdateFileShareWithEtagCallCount() int { + fake.updateFileShareWithEtagMutex.RLock() + defer fake.updateFileShareWithEtagMutex.RUnlock() + return len(fake.updateFileShareWithEtagArgsForCall) +} + +func (fake *FileShareService) UpdateFileShareWithEtagCalls(stub func(string, string, *models.Share, *zap.Logger) error) { + fake.updateFileShareWithEtagMutex.Lock() + defer fake.updateFileShareWithEtagMutex.Unlock() + fake.UpdateFileShareWithEtagStub = stub +} + +func (fake *FileShareService) UpdateFileShareWithEtagArgsForCall(i int) (string, string, *models.Share, *zap.Logger) { + fake.updateFileShareWithEtagMutex.RLock() + defer fake.updateFileShareWithEtagMutex.RUnlock() + argsForCall := fake.updateFileShareWithEtagArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 +} + +func (fake *FileShareService) UpdateFileShareWithEtagReturns(result1 error) { + fake.updateFileShareWithEtagMutex.Lock() + defer fake.updateFileShareWithEtagMutex.Unlock() + fake.UpdateFileShareWithEtagStub = nil + fake.updateFileShareWithEtagReturns = struct { + result1 error + }{result1} +} + +func (fake *FileShareService) UpdateFileShareWithEtagReturnsOnCall(i int, result1 error) { + fake.updateFileShareWithEtagMutex.Lock() + defer fake.updateFileShareWithEtagMutex.Unlock() + fake.UpdateFileShareWithEtagStub = nil + if fake.updateFileShareWithEtagReturnsOnCall == nil { + fake.updateFileShareWithEtagReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.updateFileShareWithEtagReturnsOnCall[i] = struct { + result1 error + }{result1} +} + func (fake *FileShareService) UpdateVolume(arg1 *provider.UpdatePVC, arg2 *zap.Logger) error { fake.updateVolumeMutex.Lock() ret, specificReturn := fake.updateVolumeReturnsOnCall[len(fake.updateVolumeArgsForCall)] @@ -1149,6 +1311,8 @@ func (fake *FileShareService) Invocations() map[string][][]interface{} { defer fake.getFileShareMutex.RUnlock() fake.getFileShareByNameMutex.RLock() defer fake.getFileShareByNameMutex.RUnlock() + fake.getFileShareEtagMutex.RLock() + defer fake.getFileShareEtagMutex.RUnlock() fake.getFileShareTargetMutex.RLock() defer fake.getFileShareTargetMutex.RUnlock() fake.getFileShareTargetByNameMutex.RLock() @@ -1161,6 +1325,8 @@ func (fake *FileShareService) Invocations() map[string][][]interface{} { defer fake.listSecurityGroupsMutex.RUnlock() fake.listSubnetsMutex.RLock() defer fake.listSubnetsMutex.RUnlock() + fake.updateFileShareWithEtagMutex.RLock() + defer fake.updateFileShareWithEtagMutex.RUnlock() fake.updateVolumeMutex.RLock() defer fake.updateVolumeMutex.RUnlock() copiedInvocations := map[string][][]interface{}{} diff --git a/common/vpcclient/vpcfilevolume/file_share_service.go b/common/vpcclient/vpcfilevolume/file_share_service.go index e87b56c1..65b5758d 100644 --- a/common/vpcclient/vpcfilevolume/file_share_service.go +++ b/common/vpcclient/vpcfilevolume/file_share_service.go @@ -45,6 +45,12 @@ type FileShareManager interface { // Get the file share by using share name GetFileShareByName(shareName string, ctxLogger *zap.Logger) (*models.Share, error) + // Get the file share etag by using ID + GetFileShareEtag(shareID string, ctxLogger *zap.Logger) (*models.Share, string, error) + + // UpdateFileShareWithEtag updates the shares with tags by passing etag in header + UpdateFileShareWithEtag(shareID string, etag string, shareTemplate *models.Share, ctxLogger *zap.Logger) error + // Delete the file share DeleteFileShare(shareID string, ctxLogger *zap.Logger) error diff --git a/common/vpcclient/vpcfilevolume/get_file_share_etag.go b/common/vpcclient/vpcfilevolume/get_file_share_etag.go new file mode 100644 index 00000000..7fe6680e --- /dev/null +++ b/common/vpcclient/vpcfilevolume/get_file_share_etag.go @@ -0,0 +1,55 @@ +/** + * Copyright 2025 IBM Corp. + * + * 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 vpcfilevolume ... +package vpcfilevolume + +import ( + "time" + + "github.com/IBM/ibmcloud-volume-file-vpc/common/vpcclient/client" + "github.com/IBM/ibmcloud-volume-file-vpc/common/vpcclient/models" + util "github.com/IBM/ibmcloud-volume-interface/lib/utils" + "go.uber.org/zap" +) + +// GetFileShare GET to /shares/{share-id} +func (vs *FileShareService) GetFileShareEtag(shareID string, ctxLogger *zap.Logger) (*models.Share, string, error) { + ctxLogger.Debug("Entry Backend GetFileShareEtag") + defer ctxLogger.Debug("Exit Backend GetFileShareEtag") + + defer util.TimeTracker("GetFileShareEtag", time.Now()) + + operation := &client.Operation{ + Name: "GetFileShareEtag", + Method: "GET", + PathPattern: shareIDPath, + } + + var share models.Share + var apiErr models.Error + + request := vs.client.NewRequest(operation) + ctxLogger.Info("Equivalent curl command", zap.Reflect("URL", request.URL()), zap.Reflect("Operation", operation)) + + req := request.PathParameter(shareIDParam, shareID) + resp, err := req.JSONSuccess(&share).JSONError(&apiErr).Invoke() + if err != nil { + return nil, "", err + } + + return &share, resp.Header.Get("etag"), nil +} diff --git a/common/vpcclient/vpcfilevolume/get_file_share_etag_test.go b/common/vpcclient/vpcfilevolume/get_file_share_etag_test.go new file mode 100644 index 00000000..2c0a4011 --- /dev/null +++ b/common/vpcclient/vpcfilevolume/get_file_share_etag_test.go @@ -0,0 +1,103 @@ +/** + * Copyright 2025 IBM Corp. + * + * 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 vpcfilevolume_test + +import ( + "net/http" + "testing" + + "github.com/IBM/ibmcloud-volume-file-vpc/common/vpcclient/models" + "github.com/IBM/ibmcloud-volume-file-vpc/common/vpcclient/riaas/test" + "github.com/IBM/ibmcloud-volume-file-vpc/common/vpcclient/vpcfilevolume" + "github.com/stretchr/testify/assert" + "go.uber.org/zap" +) + +func TestGetFileShareEtag(t *testing.T) { + // Setup new style zap logger + logger, _ := GetTestContextLogger() + defer logger.Sync() + + testCases := []struct { + name string + + // Response + status int + content string + + // Expected return + expectErr string + verify func(*testing.T, *models.Share, error) + }{ + { + name: "Verify that the correct endpoint is invoked", + status: http.StatusNoContent, + }, { + name: "Verify that a 404 is returned to the caller", + status: http.StatusNotFound, + content: "{\"errors\":[{\"message\":\"testerr\"}]}", + expectErr: "Trace Code:, testerr. ", + }, { + name: "Verify that the share is parsed correctly", + status: http.StatusOK, + content: "{\"id\":\"vol1\",\"name\":\"vol1\",\"size\":10,\"iops\":3000,\"status\":\"pending\",\"zone\":{\"name\":\"test-1\",\"href\":\"https://us-south.iaas.cloud.ibm.com/v1/regions/us-south/zones/test-1\"},\"crn\":\"crn:v1:bluemix:public:is:test-1:a/rg1::share:vol1\"}", + verify: func(t *testing.T, share *models.Share, err error) { + if assert.NotNil(t, share) { + assert.Equal(t, "vol1", share.ID) + } + }, + }, { + name: "False positive: What if the share ID is not matched", + status: http.StatusOK, + content: "{\"id\":\"wrong-vol\",\"name\":\"wrong-vol\",\"size\":10,\"iops\":3000,\"status\":\"pending\",\"zone\":{\"name\":\"test-1\",\"href\":\"https://us-south.iaas.cloud.ibm.com/v1/regions/us-south/zones/test-1\"},\"crn\":\"crn:v1:bluemix:public:is:test-1:a/rg1::share:wrong-vol\"}", + verify: func(t *testing.T, share *models.Share, err error) { + if assert.NotNil(t, share) { + assert.NotEqual(t, "vol1", share.ID) + } + }, + }, + } + + for _, testcase := range testCases { + t.Run(testcase.name, func(t *testing.T) { + mux, client, teardown := test.SetupServer(t) + emptyString := "" + test.SetupMuxResponse(t, mux, vpcfilevolume.Version+"/shares/share-id", http.MethodGet, &emptyString, testcase.status, testcase.content, nil) + + defer teardown() + + logger.Info("Test case being executed", zap.Reflect("testcase", testcase.name)) + + shareService := vpcfilevolume.New(client) + + share, _, err := shareService.GetFileShareEtag("share-id", logger) + logger.Info("Share details", zap.Reflect("share", share)) + + if testcase.expectErr != "" && assert.Error(t, err) { + assert.Equal(t, testcase.expectErr, err.Error()) + assert.Nil(t, share) + } else { + assert.NoError(t, err) + assert.NotNil(t, share) + } + + if testcase.verify != nil { + testcase.verify(t, share, err) + } + }) + } +} diff --git a/common/vpcclient/vpcfilevolume/update_file_share_with_etag.go b/common/vpcclient/vpcfilevolume/update_file_share_with_etag.go new file mode 100644 index 00000000..3047744f --- /dev/null +++ b/common/vpcclient/vpcfilevolume/update_file_share_with_etag.go @@ -0,0 +1,56 @@ +/** + * Copyright 2025 IBM Corp. + * + * 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 vpcvolume ... +package vpcfilevolume + +import ( + "time" + + "github.com/IBM/ibmcloud-volume-file-vpc/common/vpcclient/client" + "github.com/IBM/ibmcloud-volume-file-vpc/common/vpcclient/models" + util "github.com/IBM/ibmcloud-volume-interface/lib/utils" + "go.uber.org/zap" +) + +// UpdateVolume PATCH to /shares for updating user tags only +func (vs *FileShareService) UpdateFileShareWithEtag(shareID string, etag string, shareTemplate *models.Share, ctxLogger *zap.Logger) error { + ctxLogger.Debug("Entry Backend UpdateVolumeWithEtag") + defer ctxLogger.Debug("Exit Backend UpdateVolumeWithEtag") + + defer util.TimeTracker("UpdateVolumeWithEtag", time.Now()) + + operation := &client.Operation{ + Name: "UpdateFileShare", + Method: "PATCH", + PathPattern: shareIDPath, + } + + var apiErr models.Error + + request := vs.client.NewRequest(operation) + request.SetHeader("If-Match", etag) + + req := request.PathParameter(shareIDParam, shareID) + ctxLogger.Info("Equivalent curl command and payload details", zap.Reflect("URL", req.URL()), zap.Reflect("Payload", shareTemplate), zap.Reflect("Operation", operation)) + _, err := req.JSONBody(shareTemplate).JSONError(&apiErr).Invoke() + + if err != nil { + return err + } + + return nil +} diff --git a/common/vpcclient/vpcfilevolume/update_file_share_with_etag_test.go b/common/vpcclient/vpcfilevolume/update_file_share_with_etag_test.go new file mode 100644 index 00000000..829dd22e --- /dev/null +++ b/common/vpcclient/vpcfilevolume/update_file_share_with_etag_test.go @@ -0,0 +1,85 @@ +/** + * Copyright 2025 IBM Corp. + * + * 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 vpcfilevolume ... +package vpcfilevolume_test + +import ( + "net/http" + "testing" + + "github.com/IBM/ibmcloud-volume-file-vpc/common/vpcclient/models" + "github.com/IBM/ibmcloud-volume-file-vpc/common/vpcclient/riaas/test" + "github.com/IBM/ibmcloud-volume-file-vpc/common/vpcclient/vpcfilevolume" + "github.com/stretchr/testify/assert" + "go.uber.org/zap" +) + +func TestUpdateFileShareWithEtag(t *testing.T) { + // Setup new style zap logger + logger, _ := GetTestContextLogger() + defer logger.Sync() + + testCases := []struct { + name string + // Response + status int + content string + // Expected return + expectErr string + verify func(*testing.T, *models.Share, error) + }{ + { + name: "Verify that the correct endpoint is invoked", + status: http.StatusNoContent, + }, + { + name: "Verify that the volume expanded correctly if correct size is given", + status: http.StatusOK, + }, + { + name: "Verify that a 404 is returned to the caller", + status: http.StatusNotFound, + content: "{\"errors\":[{\"message\":\"testerr\"}]}", + expectErr: "Trace Code:, testerr. ", + }, + } + + for _, testcase := range testCases { + t.Run(testcase.name, func(t *testing.T) { + template := &models.Share{ + ID: "share-id", + UserTags: []string{"tag1:val1", "tag2:val2"}, + } + mux, client, teardown := test.SetupServer(t) + test.SetupMuxResponse(t, mux, vpcfilevolume.Version+"/shares/share-id", http.MethodPatch, nil, testcase.status, testcase.content, nil) + logger.Info("tested SetupMuxResponse") + defer teardown() + + logger.Info("Test case being executed", zap.Reflect("testcase", testcase.name)) + + volumeService := vpcfilevolume.New(client) + + err := volumeService.UpdateFileShareWithEtag("share-id", "xyz", template, logger) + + if testcase.expectErr != "" && assert.Error(t, err) { + assert.Equal(t, testcase.expectErr, err.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/e2e/README.md b/e2e/README.md index 2a7893fb..b45626f6 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -15,6 +15,8 @@ export IC_API_KEY_PROD= | export IC_API_KEY_STAG= export e2e_addon_version=<1.2 or 2.0> export icrImage= + export SC= + export SC_RETAIN= # Optional export E2E_POD_COUNT="1" diff --git a/e2e/e2e.sh b/e2e/e2e.sh index 6aa66fed..fd5b55e3 100755 --- a/e2e/e2e.sh +++ b/e2e/e2e.sh @@ -158,7 +158,7 @@ fi # E2E Execution go clean -modcache export GO111MODULE=on -go install -mod=mod github.com/onsi/ginkgo/v2/ginkgo@v2.17.2 +go install -mod=mod github.com/onsi/ginkgo/v2/ginkgo@v2.19.0 set +e # Non EIT based tests diff --git a/e2e/iks-cluster b/e2e/iks-cluster index 94210bc6..4c83604c 100755 --- a/e2e/iks-cluster +++ b/e2e/iks-cluster @@ -414,6 +414,10 @@ function cluster_create_vpc { fi if [[ $rc -ne 0 ]]; then set +x; return 1; fi + echo "Sleeping for 30 sec after cluster creation..." + # TODO: Add iteration to check worker pool creation for 3 iteration every 10 sec + sleep 30 # Sometimes ks worker-pool create command fails with error 'Unable to find cluster' + if [[ $E2ETEST_MZ == "true" ]]; then random_string=${cluster_name%%-*} cluster_worker_pool="e2etest-${random_string}" diff --git a/e2e/testsuites/baseutils.go b/e2e/testsuites/baseutils.go index 6b6a310d..e32716f8 100644 --- a/e2e/testsuites/baseutils.go +++ b/e2e/testsuites/baseutils.go @@ -672,7 +672,7 @@ func generatePVC(name, namespace, AccessModes: []v1.PersistentVolumeAccessMode{ accessMode, }, - Resources: v1.ResourceRequirements{ + Resources: v1.VolumeResourceRequirements{ Requests: v1.ResourceList{ v1.ResourceName(v1.ResourceStorage): resource.MustParse(claimSize), }, @@ -1159,7 +1159,7 @@ func (t *TestPod) Exec(command []string, expectedString string) { func (t *TestPod) WaitForSuccess() { By(fmt.Sprintf("checking that the pods command exits with no error [%s/%s]", t.namespace.Name, t.pod.Name)) - err := k8sDevPod.WaitForPodSuccessInNamespaceSlow(context.TODO(), t.client, t.pod.Name, t.namespace.Name) + err := k8sDevPod.WaitForPodSuccessInNamespace(context.TODO(), t.client, t.pod.Name, t.namespace.Name) framework.ExpectNoError(err) } diff --git a/file/provider/session_test.go b/file/provider/session_test.go index bc2d2737..0d86aa06 100644 --- a/file/provider/session_test.go +++ b/file/provider/session_test.go @@ -58,12 +58,3 @@ func TestGetProviderDisplayName(t *testing.T) { assert.Equal(t, provider.VolumeProvider("VPC-SHARE"), ccf.GetProviderDisplayName()) } - -func TestUpdateVolume(t *testing.T) { - ccf := &VPCSession{ - VolumeType: provider.VolumeType("vpc-share"), - Provider: provider.VolumeProvider("VPC-SHARE"), - } - - assert.NotNil(t, ccf.UpdateVolume(provider.Volume{})) -} diff --git a/file/provider/update_volume.go b/file/provider/update_volume.go index 0dbe228b..ec8309dc 100644 --- a/file/provider/update_volume.go +++ b/file/provider/update_volume.go @@ -18,12 +18,72 @@ package provider import ( - "errors" + "strings" + userError "github.com/IBM/ibmcloud-volume-file-vpc/common/messages" + "github.com/IBM/ibmcloud-volume-file-vpc/common/vpcclient/models" "github.com/IBM/ibmcloud-volume-interface/lib/provider" + "go.uber.org/zap" ) -// UpdateVolume POSTs to /volumes -func (vpc *VPCSession) UpdateVolume(volumeTemplate provider.Volume) error { - return errors.New("unsupported Operation") +// UpdateVolume PATCH to /volumes +func (vpcs *VPCSession) UpdateVolume(volumeTemplate provider.Volume) error { + var existShare *models.Share + var err error + var etag string + + //Fetch existing volume Tags + err = retryWithMinRetries(vpcs.Logger, func() error { + // Get volume details + existShare, etag, err = vpcs.Apiclient.FileShareService().GetFileShareEtag(volumeTemplate.VolumeID, vpcs.Logger) + + if err != nil { + return err + } + + if existShare != nil && existShare.Status == StatusStable { + vpcs.Logger.Info("Volume got valid (available) state", zap.Reflect("etag", etag)) + } else { + return userError.GetUserError("VolumeNotInValidState", err, volumeTemplate.VolumeID) + } + + //If tags are equal then skip the UpdateFileShare RIAAS API call + if ifTagsEqual(existShare.UserTags, volumeTemplate.VPCVolume.Tags) { + vpcs.Logger.Info("There is no change in user tags for volume, skipping the updateVolume for VPC IaaS... ", zap.Reflect("existShare", existShare.UserTags), zap.Reflect("volumeRequest", volumeTemplate.VPCVolume.Tags)) + return nil + } + + //Append the existing tags with the requested input tags + existShare.UserTags = append(existShare.UserTags, volumeTemplate.VPCVolume.Tags...) + + volume := &models.Share{ + UserTags: existShare.UserTags, + } + + vpcs.Logger.Info("Calling VPC provider for volume UpdateVolumeWithTags...") + + err = vpcs.Apiclient.FileShareService().UpdateFileShareWithEtag(volumeTemplate.VolumeID, etag, volume, vpcs.Logger) + return err + }) + + if err != nil { + vpcs.Logger.Error("Failed to update volume tags from VPC provider", zap.Reflect("BackendError", err)) + return userError.GetUserError("FailedToUpdateVolume", err, volumeTemplate.VolumeID) + } + + return err +} + +// ifTagsEqual will check if there is change to existing tags +func ifTagsEqual(existingTags []string, newTags []string) bool { + //Join slice into a string + tags := strings.ToLower(strings.Join(existingTags, ",")) + for _, v := range newTags { + if !strings.Contains(tags, strings.ToLower(v)) { + //Tags are different + return false + } + } + //Tags are equal + return true } diff --git a/file/provider/update_volume_test.go b/file/provider/update_volume_test.go new file mode 100644 index 00000000..25b63c51 --- /dev/null +++ b/file/provider/update_volume_test.go @@ -0,0 +1,156 @@ +/** + * Copyright 2022 IBM Corp. + * + * 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 ( + "errors" + "fmt" + "testing" + + "github.com/IBM/ibmcloud-volume-file-vpc/common/vpcclient/models" + volumeServiceFakes "github.com/IBM/ibmcloud-volume-file-vpc/common/vpcclient/vpcfilevolume/fakes" + "github.com/IBM/ibmcloud-volume-interface/lib/provider" + util "github.com/IBM/ibmcloud-volume-interface/lib/utils" + "github.com/IBM/ibmcloud-volume-interface/lib/utils/reasoncode" + "github.com/stretchr/testify/assert" + "go.uber.org/zap" +) + +func TestUpdateVolume(t *testing.T) { + logger, teardown := GetTestLogger(t) + defer teardown() + + var ( + fileShareService *volumeServiceFakes.FileShareService + ) + + testCases := []struct { + testCaseName string + volumeID string + tags []string + baseVolume *models.Share + etag string + newSize int64 + expectedErr string + expectedSize int64 + expectedReasonCode string + }{ + { + testCaseName: "OK", + volumeID: "16f293bf-test-4bff-816f-e199c0c65db5", + etag: "abc", + baseVolume: &models.Share{ + ID: "16f293bf-test-4bff-816f-e199c0c65db5", + Status: models.StatusType("stable"), + Size: int64(10), + Iops: int64(1000), + UserTags: []string{"tag3:val3"}, + Zone: &models.Zone{Name: "test-zone"}, + }, + tags: []string{"tag1:val1", "tag2:val2"}, + }, + { + testCaseName: "Tags are equal", + volumeID: "16f293bf-test-4bff-816f-e199c0c65db5", + etag: "abc", + baseVolume: &models.Share{ + ID: "16f293bf-test-4bff-816f-e199c0c65db5", + Status: models.StatusType("stable"), + Size: int64(10), + Iops: int64(1000), + UserTags: []string{"tag1:val1", "tag2:val2"}, + Zone: &models.Zone{Name: "test-zone"}, + }, + tags: []string{"tag1:val1", "tag2:val2"}, + }, + { + testCaseName: "New tags added", + etag: "abc", + volumeID: "16f293bf-test-4bff-816f-e199c0c65db5", + baseVolume: &models.Share{ + ID: "16f293bf-test-4bff-816f-e199c0c65db5", + Status: models.StatusType("stable"), + Size: int64(10), + Iops: int64(1000), + UserTags: []string{"tag3:val3"}, + Zone: &models.Zone{Name: "test-zone"}, + }, + tags: []string{"tag1:val1", "tag2:val2"}, + }, + { + testCaseName: "Volume is not available for update", + etag: "abc", + volumeID: "16f293bf-test-4bff-816f-e199c0c65db5", + baseVolume: &models.Share{ + ID: "16f293bf-test-4bff-816f-e199c0c65db5", + Status: models.StatusType("updating"), + Size: int64(10), + Iops: int64(1000), + Zone: &models.Zone{Name: "test-zone"}, + }, + tags: []string{"tag1:val1", "tag2:val2"}, + expectedErr: "{Code:ErrorUnclassified, Type:VolumeNotInValidState, Description:Volume did not get valid (available) status within timeout period., BackendError:, RC:500}", + expectedReasonCode: "ErrorUnclassified", + }, + + { + testCaseName: "volume not found", + volumeID: "16f293bf-test-4bff-816f-e199c0c65db5", + baseVolume: nil, + expectedErr: "{Code:ErrorUnclassified, Type:InvalidRequest, Description:'Wrong volume ID' volume ID is not valid. Please check https://cloud.ibm.com/docs/infrastructure/vpc?topic=vpc-rias-error-messages#volume_id_invalid, BackendError:, RC:400}", + expectedReasonCode: "ErrorUnclassified", + }, + } + + for _, testcase := range testCases { + t.Run(testcase.testCaseName, func(t *testing.T) { + logger.Info("Started") + vpcs, uc, sc, err := GetTestOpenSession(t, logger) + assert.NotNil(t, vpcs) + assert.NotNil(t, uc) + assert.NotNil(t, sc) + assert.Nil(t, err) + + fileShareService = &volumeServiceFakes.FileShareService{} + fmt.Println("Success volumeshareservice") + assert.NotNil(t, fileShareService) + uc.FileShareServiceReturns(fileShareService) + + if testcase.expectedErr != "" { + fileShareService.GetFileShareEtagReturns(testcase.baseVolume, testcase.etag, errors.New(testcase.expectedReasonCode)) + fileShareService.UpdateFileShareWithEtagReturns(errors.New(testcase.expectedReasonCode)) + } else { + fileShareService.GetFileShareEtagReturns(testcase.baseVolume, testcase.etag, nil) + fileShareService.UpdateFileShareWithEtagReturns(nil) + } + + requestExp := provider.Volume{VolumeID: testcase.volumeID, + VPCVolume: provider.VPCVolume{Tags: testcase.tags}} + + err = vpcs.UpdateVolume(requestExp) + + if testcase.expectedErr != "" { + assert.NotNil(t, err) + logger.Info("Error details", zap.Reflect("Error details", err.Error())) + assert.Equal(t, reasoncode.ReasonCode(testcase.expectedReasonCode), util.ErrorReasonCode(err)) + } else { + assert.Nil(t, err) + } + }) + } + +} diff --git a/file/provider/util.go b/file/provider/util.go index 56e80787..3c54ac0a 100644 --- a/file/provider/util.go +++ b/file/provider/util.go @@ -30,6 +30,9 @@ import ( // maxRetryAttempt ... var maxRetryAttempt = 10 +// minRetryAttempt ... +var minRetryAttempt = 5 + // maxRetryGap ... var maxRetryGap = 60 @@ -112,6 +115,41 @@ func retry(logger *zap.Logger, retryfunc func() error) error { return err } +// retry ... +func retryWithMinRetries(logger *zap.Logger, retryfunc func() error) error { + var err error + retryGap := 10 + for i := 0; i < minRetryAttempt; i++ { + if i > 0 { + time.Sleep(time.Duration(retryGap) * time.Second) + } + err = retryfunc() + if err != nil { + logger.Info("err object is not nil", zap.Reflect("ERR", err)) + //Skip retry for the below type of Errors + modelError, ok := err.(*models.Error) + if !ok { + continue + } + if skipRetry(modelError) { + break + } + if i >= 1 { + retryGap = 2 * retryGap + if retryGap > maxRetryGap { + retryGap = maxRetryGap + } + } + if (i + 1) < minRetryAttempt { + logger.Info("Error while executing the function. Re-attempting execution ..", zap.Int("attempt..", i+2), zap.Int("retry-gap", retryGap), zap.Int("max-retry-Attempts", minRetryAttempt), zap.Error(err)) + } + continue + } + return err + } + return err +} + // skipRetry skip retry as per listed error codes func skipRetry(err *models.Error) bool { for _, errorItem := range err.Errors { diff --git a/go.mod b/go.mod index 684bd18f..c29cb185 100644 --- a/go.mod +++ b/go.mod @@ -4,14 +4,14 @@ go 1.22.0 require ( github.com/IBM-Cloud/ibm-cloud-cli-sdk v0.6.7 - github.com/IBM/ibm-csi-common v1.1.15 - github.com/IBM/ibmcloud-volume-interface v1.2.6 + github.com/IBM/ibmcloud-volume-interface v1.2.9 github.com/IBM/secret-common-lib v1.1.11 github.com/IBM/secret-utils-lib v1.1.11 github.com/IBM/vpc-beta-go-sdk v0.8.0 github.com/fatih/structs v1.1.0 github.com/gofrs/uuid v4.4.0+incompatible github.com/golang-jwt/jwt/v4 v4.5.0 + github.com/golang/glog v1.2.1 github.com/kubernetes-csi/external-snapshotter/client/v4 v4.2.0 github.com/onsi/ginkgo/v2 v2.19.0 github.com/onsi/gomega v1.33.1 @@ -26,7 +26,7 @@ require ( require ( github.com/NYTimes/gziphandler v1.1.1 // indirect - github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/coreos/go-semver v0.3.1 // indirect @@ -76,6 +76,7 @@ require ( k8s.io/component-base v0.30.4 // indirect k8s.io/component-helpers v0.30.4 // indirect k8s.io/controller-manager v0.30.4 // indirect + k8s.io/csi-translation-lib v0.30.4 // indirect k8s.io/kms v0.30.4 // indirect k8s.io/kubectl v0.30.4 // indirect k8s.io/kubelet v0.30.4 // indirect @@ -89,7 +90,6 @@ require ( github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/container-storage-interface/spec v1.9.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect @@ -149,3 +149,9 @@ require ( sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) + +replace ( + github.com/google/cel-go => github.com/google/cel-go v0.17.8 + github.com/prometheus/client_golang => github.com/prometheus/client_golang v1.18.0 + github.com/prometheus/common => github.com/prometheus/common v0.47.0 +) diff --git a/go.sum b/go.sum index eba8d97e..e50160bf 100644 --- a/go.sum +++ b/go.sum @@ -30,10 +30,8 @@ github.com/IBM-Cloud/ibm-cloud-cli-sdk v0.6.7 h1:eHgfQl6IeSmzWUyiSi13CvoFYsovoyq github.com/IBM-Cloud/ibm-cloud-cli-sdk v0.6.7/go.mod h1:RiUvKuHKTBmBApDMUQzBL14pQUGKcx/IioKQPIcRQjs= github.com/IBM/go-sdk-core/v5 v5.17.4 h1:VGb9+mRrnS2HpHZFM5hy4J6ppIWnwNrw0G+tLSgcJLc= github.com/IBM/go-sdk-core/v5 v5.17.4/go.mod h1:KsAAI7eStAWwQa4F96MLy+whYSh39JzNjklZRbN/8ns= -github.com/IBM/ibm-csi-common v1.1.15 h1:oJ0NsrVBqfXKCZXSc/EI+s7/RlUK/TpzTI+jcdoAqpI= -github.com/IBM/ibm-csi-common v1.1.15/go.mod h1:oKYsmovJ4HAmbHfhQvVn1dJ+xeQHqaa3hQsN/Z5itlc= -github.com/IBM/ibmcloud-volume-interface v1.2.6 h1:OLumrSQ0XTOp6gW+k0z2X53uTYOIt1wWSkTCXzK/vAM= -github.com/IBM/ibmcloud-volume-interface v1.2.6/go.mod h1:sDeQiPuN8k9yTRl9FbE2GZCXPNg4cV3oldUfL8wwGNA= +github.com/IBM/ibmcloud-volume-interface v1.2.9 h1:ug55V2mzK/IaFkfuKDOt74yzhLapSR/+qVgfQblfAjw= +github.com/IBM/ibmcloud-volume-interface v1.2.9/go.mod h1:sDeQiPuN8k9yTRl9FbE2GZCXPNg4cV3oldUfL8wwGNA= github.com/IBM/secret-common-lib v1.1.11 h1:EpfEe1gT1bnFQ3bxQPrh6bzTPeGjUo1NReVkCCP+TOc= github.com/IBM/secret-common-lib v1.1.11/go.mod h1:7YJF0ipT979nHIPkiCpvjFboFoIhrmYnIliE1vjCjZM= github.com/IBM/secret-utils-lib v1.1.11 h1:w87BzkddoFFlhRuWRteuGj3/561lEUg6Oo0RajVC87A= @@ -49,8 +47,8 @@ github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= -github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df h1:7RFfzj4SSt6nnvCPbCqijJi1nWCd+TqAT3bYCStRC18= +github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= @@ -68,8 +66,6 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/container-storage-interface/spec v1.9.0 h1:zKtX4STsq31Knz3gciCYCi1SXtO2HJDecIjDVboYavY= -github.com/container-storage-interface/spec v1.9.0/go.mod h1:ZfDu+3ZRyeVqxZM0Ds19MVLkN2d1XJ5MAfi1L3VjlT0= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= @@ -163,6 +159,8 @@ github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzq github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4= +github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= @@ -187,8 +185,8 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= -github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84= -github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg= +github.com/google/cel-go v0.17.8 h1:j9m730pMZt1Fc4oKhCLUHfjj6527LuhYcYw0Rl8gqto= +github.com/google/cel-go v0.17.8/go.mod h1:HXZKzB0LXqer5lHHgfWAnlYwJaQBDKMjxjulNQzhwhY= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -323,13 +321,13 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= -github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= +github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= -github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/common v0.47.0 h1:p5Cz0FNHo7SnWOmWmoRozVcjEp0bIVU8cV7OShpjL1k= +github.com/prometheus/common v0.47.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -564,8 +562,8 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20231030173426-d783a09b4405 h1:I6WNifs6pF9tNdSob2W24JtyxIYjzFB9qDlpUC76q+U= -google.golang.org/genproto v0.0.0-20231030173426-d783a09b4405/go.mod h1:3WDQMjmJk36UQhjQ89emUzb1mdaHcPeeAh4SCBKznB4= +google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY= +google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw= google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU= google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= diff --git a/pkg/metadata/metadata.go b/pkg/metadata/metadata.go index 012df49d..bc7e7d22 100644 --- a/pkg/metadata/metadata.go +++ b/pkg/metadata/metadata.go @@ -22,13 +22,29 @@ import ( "fmt" "strings" - "github.com/IBM/ibm-csi-common/pkg/utils" "go.uber.org/zap" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" ) +const ( + // NodeZoneLabel Zone Label attached to node + NodeZoneLabel = "failure-domain.beta.kubernetes.io/zone" + + // NodeRegionLabel Region Label attached to node + NodeRegionLabel = "failure-domain.beta.kubernetes.io/region" + + // NodeInstanceIDLabel VPC ID label attached to satellite host + NodeInstanceIDLabel = "ibm-cloud.kubernetes.io/vpc-instance-id" + + // MachineTypeLabel is the node label used to identify the cluster type (upi,ipi,etc) + MachineTypeLabel = "ibm-cloud.kubernetes.io/machine-type" + + // UPI is the expected value assigned to machine-type label on satellite cluster nodes + UPI = "upi" +) + // NodeMetadata is a fakeable interface exposing necessary data type NodeMetadata interface { // GetZone ... @@ -67,16 +83,16 @@ func NewNodeMetadata(nodeName string, logger *zap.Logger) (NodeMetadata, error) } nodeLabels := node.ObjectMeta.Labels - if len(nodeLabels[utils.NodeRegionLabel]) == 0 || len(nodeLabels[utils.NodeZoneLabel]) == 0 { - errorMsg := fmt.Errorf("One or few required node label(s) is/are missing [%s, %s]. Node Labels Found = [#%v]", utils.NodeRegionLabel, utils.NodeZoneLabel, nodeLabels) //nolint:golint + if len(nodeLabels[NodeRegionLabel]) == 0 || len(nodeLabels[NodeZoneLabel]) == 0 { + errorMsg := fmt.Errorf("One or few required node label(s) is/are missing [%s, %s]. Node Labels Found = [#%v]", NodeRegionLabel, NodeZoneLabel, nodeLabels) //nolint:golint return nil, errorMsg } var workerID string // If the cluster is satellite, the machine-type label equals to UPI - if nodeLabels[utils.MachineTypeLabel] == utils.UPI { + if nodeLabels[MachineTypeLabel] == UPI { // For a satellite cluster, workerID is fetched from vpc-instance-id node label, which is updated by the vpc-node-label-updater (init container) - workerID = nodeLabels[utils.NodeInstanceIDLabel] + workerID = nodeLabels[NodeInstanceIDLabel] } else { // For managed and IPI cluster, workerID is fetched from the ProviderID in node spec. workerID = fetchInstanceID(node.Spec.ProviderID) @@ -86,8 +102,8 @@ func NewNodeMetadata(nodeName string, logger *zap.Logger) (NodeMetadata, error) } return &nodeMetadataManager{ - zone: nodeLabels[utils.NodeZoneLabel], - region: nodeLabels[utils.NodeRegionLabel], + zone: nodeLabels[NodeZoneLabel], + region: nodeLabels[NodeRegionLabel], workerID: workerID, }, nil } diff --git a/pkg/watcher/pv_watcher.go b/pkg/watcher/pv_watcher.go new file mode 100644 index 00000000..7f48c500 --- /dev/null +++ b/pkg/watcher/pv_watcher.go @@ -0,0 +1,344 @@ +/** + * Copyright 2025 IBM Corp. + * + * 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 watcher ... +package watcher + +import ( + "flag" + "os" + "strings" + "time" + + uid "github.com/gofrs/uuid" + "go.uber.org/zap/zapcore" + + "github.com/golang/glog" + + iks_vpc_provider "github.com/IBM/ibmcloud-volume-file-vpc/iks/provider" + cloudprovider "github.com/IBM/ibmcloud-volume-file-vpc/pkg/ibmcloudprovider" + "github.com/IBM/ibmcloud-volume-interface/config" + "github.com/IBM/ibmcloud-volume-interface/lib/provider" + + "go.uber.org/zap" + "golang.org/x/net/context" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/scheme" + v1core "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/tools/record" +) + +// PVWatcher to watch pv creation and add taggs +type PVWatcher struct { + logger *zap.Logger + kclient kubernetes.Interface + config *config.Config + provisionerName string + recorder record.EventRecorder + cloudProvider cloudprovider.CloudProviderInterface +} + +const ( + //IbmCloudGtAPIEndpoint ... + IbmCloudGtAPIEndpoint = "IBMCLOUD_GT_API_ENDPOINT" + //ReclaimPolicyTag ... + ReclaimPolicyTag = "reclaimpolicy:" + //NameSpaceTag ... + NameSpaceTag = "namespace:" + //StorageClassTag ... + StorageClassTag = "storageclass:" + //PVCNameTag ... + PVCNameTag = "pvc:" + //PVNameTag ... + PVNameTag = "pv:" + //VolumeCRN ... + VolumeCRN = "volumeCRN" + //ProvisionerTag ... + ProvisionerTag = "provisioner:" + + //VolumeStatus ... + VolumeStatus = "status" + //VolumeStatusCreated ... + VolumeStatusCreated = "created" + //VolumeStatusDeleted ... + VolumeStatusDeleted = "deleted" + //VolumeUpdateEventReason ... + VolumeUpdateEventReason = "VolumeMetaDataSaved" + //VolumeUpdateEventSuccess ... + VolumeUpdateEventSuccess = "Success" + + // VolumeIDLabel ... + VolumeIDLabel = "volumeId" + + // VolumeCRNLabel ... + VolumeCRNLabel = "volumeCRN" + + // ClusterIDLabel ... + ClusterIDLabel = "clusterID" + + // IOPSLabel ... + IOPSLabel = "iops" + + // ZoneLabel ... + ZoneLabel = "zone" + + // GiB in bytes + GiB = 1024 * 1024 * 1024 +) + +// VolumeTypeMap ... +var VolumeTypeMap = map[string]string{} + +var master = flag.String( + "master", + "", + "Master URL to build a client config from. Either this or kubeconfig needs to be set if the provisioner is being run out of cluster.", +) +var kubeconfig = flag.String( + "kubeconfig", + "", + "Absolute path to the kubeconfig file. Either this or master needs to be set if the provisioner is being run out of cluster.", +) + +// New creates the Watcher instance +func New(logger *zap.Logger, provisionerName string, volumeType string, cloudProvider cloudprovider.CloudProviderInterface) *PVWatcher { + var restConfig *rest.Config + var err error + // Register provider + VolumeTypeMap[provisionerName] = volumeType + + restConfig, err = clientcmd.BuildConfigFromFlags(*master, *kubeconfig) + if err != nil { + logger.Fatal("Failed to create config:", zap.Error(err)) + } + + clientset, err := kubernetes.NewForConfig(restConfig) + if err != nil { + logger.Fatal("Failed to create client:", zap.Error(err)) + } + iksPodName := os.Getenv("POD_NAME") + + broadcaster := record.NewBroadcaster() + broadcaster.StartLogging(glog.Infof) + eventInterface := clientset.CoreV1().Events("") + broadcaster.StartRecordingToSink(&v1core.EventSinkImpl{Interface: eventInterface}) + pvw := &PVWatcher{ + logger: logger, + config: cloudProvider.GetConfig(), + provisionerName: provisionerName, + kclient: clientset, + cloudProvider: cloudProvider, + recorder: broadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: iksPodName}), + } + return pvw +} + +// Start start pv watcher +func (pvw *PVWatcher) Start() { + watchlist := cache.NewListWatchFromClient(pvw.kclient.CoreV1().RESTClient(), "persistentvolumes", "", fields.Everything()) + _, controller := cache.NewInformer(watchlist, &v1.PersistentVolume{}, time.Second*0, + cache.FilteringResourceEventHandler{ + Handler: cache.ResourceEventHandlerFuncs{ + UpdateFunc: pvw.updateVolume, + }, + FilterFunc: pvw.filter, + }, + ) + pvw.logger.Info("PVWatcher starting") + stopch := wait.NeverStop + go controller.Run(stopch) + pvw.logger.Info("PVWatcher started") + <-stopch +} + +func (pvw *PVWatcher) updateVolume(oldobj, obj interface{}) { + // Run as non-blocking thread to allow parallel processing of volumes + go func() { + var oldStatus v1.PersistentVolumePhase + var newStatus v1.PersistentVolumePhase + ctxLogger, requestID := GetContextLogger(context.Background(), false) + // panic-recovery function that avoid watcher thread to stop because of unexexpected error + defer func() { + if r := recover(); r != nil { + ctxLogger.Error("Recovered from panic in pvwatcher", zap.Stack("stack"), zap.String("requestID", requestID)) + } + }() + + ctxLogger.Info("Entry updateVolume()", zap.Reflect("obj", obj), zap.Reflect("oldobj", oldobj)) + newpv, _ := obj.(*v1.PersistentVolume) + //If there is no change to status , capacity or iops we can skip the updateVolume call. + if oldobj != nil { + oldpv, _ := oldobj.(*v1.PersistentVolume) + oldCapacity := oldpv.Spec.Capacity[v1.ResourceStorage] + capacity := newpv.Spec.Capacity[v1.ResourceStorage] + iops := newpv.Spec.CSI.VolumeAttributes[IOPSLabel] + oldiops := oldpv.Spec.CSI.VolumeAttributes[IOPSLabel] + newStatus = newpv.Status.Phase + oldStatus = oldpv.Status.Phase + if (newStatus == oldStatus) && (oldCapacity.Value() == capacity.Value()) && (oldiops == iops) { + ctxLogger.Info("Skipping update Volume as there is no change in status , capacity and iops") + return + } + } + + session, err := pvw.cloudProvider.GetProviderSession(context.Background(), ctxLogger) + if session != nil { + iksVpc, ok := session.(*iks_vpc_provider.IksVpcSession) + + if !ok { + ctxLogger.Error("Failed to get the IKS-VPC session, Try to restart the CSI driver controller POD") + return + } + + volume := pvw.getVolumeFromPV(newpv, ctxLogger) + // Updating metadata for the volume + ctxLogger.Info("Updating metadata for the volume", zap.Reflect("volume", volume)) + err := iksVpc.UpdateVolume(volume) + if err != nil { + ctxLogger.Warn("Failed to update volume metadata", zap.Error(err)) + pvw.recorder.Event(newpv, v1.EventTypeWarning, VolumeUpdateEventReason, err.Error()) + } + + //Lets invoke the VPC IaaS update Volume only if there is status change and new status is bound state. + //This will be true only when PVC is first time created + if newStatus != oldStatus && newStatus == v1.VolumeBound { + ctxLogger.Info("Updating tags from VPC IaaS") + err = iksVpc.VPCSession.UpdateVolume(volume) + if err != nil { + ctxLogger.Warn("Failed to update volume with tags from VPC IaaS", zap.Error(err)) + pvw.recorder.Event(newpv, v1.EventTypeWarning, VolumeUpdateEventReason, err.Error()) + } else { + pvw.recorder.Event(newpv, v1.EventTypeNormal, VolumeUpdateEventReason, VolumeUpdateEventSuccess) + ctxLogger.Warn("Volume Metadata saved successfully") + } + } else { + ctxLogger.Info("Skipping Updating tags from VPC IaaS as there is no change in tags") + } + } + ctxLogger.Info("Exit updateVolume()", zap.Error(err)) + }() +} + +func (pvw *PVWatcher) getTags(pv *v1.PersistentVolume, ctxLogger *zap.Logger) (string, []string) { + ctxLogger.Debug("Entry getTags()", zap.Reflect("pv", pv)) + volAttributes := pv.Spec.CSI.VolumeAttributes + // Get user tag list + tagstr := strings.TrimSpace(volAttributes["tags"]) + var tags []string + if len(tagstr) > 0 { + tags = strings.Split(tagstr, ",") + } + // append default tags to users tag list + tags = append(tags, ClusterIDLabel+":"+volAttributes[ClusterIDLabel]) + tags = append(tags, ReclaimPolicyTag+string(pv.Spec.PersistentVolumeReclaimPolicy)) + tags = append(tags, StorageClassTag+pv.Spec.StorageClassName) + tags = append(tags, NameSpaceTag+pv.Spec.ClaimRef.Namespace) + tags = append(tags, PVCNameTag+pv.Spec.ClaimRef.Name) + tags = append(tags, PVNameTag+pv.ObjectMeta.Name) + tags = append(tags, ProvisionerTag+pvw.provisionerName) + ctxLogger.Debug("Exit getTags()", zap.String("VolumeCRN", volAttributes[VolumeCRN]), zap.Reflect("tags", tags)) + return volAttributes[VolumeCRN], tags +} + +func (pvw *PVWatcher) getVolumeFromPV(pv *v1.PersistentVolume, ctxLogger *zap.Logger) provider.Volume { + ctxLogger.Debug("Entry getVolume()", zap.Reflect("pv", pv)) + crn, tags := pvw.getTags(pv, ctxLogger) + volume := provider.Volume{ + VolumeID: pv.Spec.CSI.VolumeHandle, + Provider: provider.VolumeProvider(pvw.config.VPC.VPCBlockProviderType), + VolumeType: provider.VolumeType(VolumeTypeMap[pv.Spec.CSI.Driver]), + } + volume.CRN = crn + clusterID := pv.Spec.CSI.VolumeAttributes[ClusterIDLabel] + volume.Attributes = map[string]string{strings.ToLower(ClusterIDLabel): clusterID} + if pv.Status.Phase == v1.VolumeReleased { + // Set only status in case of delete operation + volume.Attributes[VolumeStatus] = VolumeStatusDeleted + } else { + volume.Tags = tags + //Get Capacity and convert to GiB + capacity := pv.Spec.Capacity[v1.ResourceStorage] + capacityGiB := BytesToGiB(capacity.Value()) + volume.Capacity = &capacityGiB + iops := pv.Spec.CSI.VolumeAttributes[IOPSLabel] + volume.Iops = &iops + volume.Attributes[VolumeStatus] = VolumeStatusCreated + } + ctxLogger.Debug("Exit getVolume()", zap.Reflect("volume", volume)) + return volume +} + +func (pvw *PVWatcher) filter(obj interface{}) bool { + pvw.logger.Debug("Entry filter()", zap.Reflect("obj", obj)) + pv, _ := obj.(*v1.PersistentVolume) + var provisoinerMatch = false + if pv != nil && pv.Spec.CSI != nil { + provisoinerMatch = pv.Spec.CSI.Driver == pvw.provisionerName + } + pvw.logger.Debug("Exit filter()", zap.Bool("provisoinerMatch", provisoinerMatch)) + return provisoinerMatch +} + +// BytesToGiB converts Bytes to GiB +func BytesToGiB(volumeSizeBytes int64) int { + return int(volumeSizeBytes / GiB) +} + +// GetContextLogger ... +func GetContextLogger(ctx context.Context, isDebug bool) (*zap.Logger, string) { + return GetContextLoggerWithRequestID(ctx, isDebug, nil) +} + +// GetContextLoggerWithRequestID adds existing requestID in the logger +// The Existing requestID might be coming from ControllerPublishVolume etc +func GetContextLoggerWithRequestID(ctx context.Context, isDebug bool, requestIDIn *string) (*zap.Logger, string) { + consoleDebugging := zapcore.Lock(os.Stdout) + consoleErrors := zapcore.Lock(os.Stderr) + encoderConfig := zap.NewProductionEncoderConfig() + encoderConfig.TimeKey = "ts" + encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder + traceLevel := zap.NewAtomicLevel() + if isDebug { + traceLevel.SetLevel(zap.DebugLevel) + } else { + traceLevel.SetLevel(zap.InfoLevel) + } + + core := zapcore.NewTee( + zapcore.NewCore(zapcore.NewJSONEncoder(encoderConfig), consoleDebugging, zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { + return (lvl >= traceLevel.Level()) && (lvl < zapcore.ErrorLevel) + })), + zapcore.NewCore(zapcore.NewJSONEncoder(encoderConfig), consoleErrors, zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { + return lvl >= zapcore.ErrorLevel + })), + ) + logger := zap.New(core, zap.AddCaller()) + // generating a unique request ID so that logs can be filter + if requestIDIn == nil { + // Generate New RequestID if not provided + uuid, _ := uid.NewV4() // #nosec G104: Attempt to randomly generate uuid + requestID := uuid.String() + requestIDIn = &requestID + } + logger = logger.With(zap.String("RequestID", *requestIDIn)) + return logger, *requestIDIn + " " +} diff --git a/pkg/watcher/pv_watcher_test.go b/pkg/watcher/pv_watcher_test.go new file mode 100644 index 00000000..4e60881c --- /dev/null +++ b/pkg/watcher/pv_watcher_test.go @@ -0,0 +1,180 @@ +/** + * Copyright 2025 IBM Corp. + * + * 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 watcher ... +package watcher + +import ( + "bytes" + "net/http" + "os" + "strings" + "testing" + + cloudprovider "github.com/IBM/ibmcloud-volume-file-vpc/pkg/ibmcloudprovider" + "github.com/IBM/ibmcloud-volume-interface/config" + "github.com/golang/glog" + "github.com/onsi/gomega/ghttp" + "github.com/stretchr/testify/assert" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/kubernetes/scheme" + v1core "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/client-go/tools/record" +) + +func TestNew(t *testing.T) { + // Creating test logger + _, teardown := GetTestLogger(t) + defer teardown() +} + +func TestAddTags(t *testing.T) { + var server *ghttp.Server + conf := &config.Config{ + Bluemix: &config.BluemixConfig{ + IamAPIKey: "test", + }, + VPC: &config.VPCProviderConfig{ + VPCBlockProviderName: "vpc-classic", + }, + } + logger, _ := GetTestLogger(t) + fakeIBMCloudStorageProvider, _ := cloudprovider.NewFakeIBMCloudStorageProvider("configPath", logger) + + broadcaster := record.NewBroadcaster() + broadcaster.StartLogging(glog.Infof) + clientset := fake.NewSimpleClientset() + eventInterface := clientset.CoreV1().Events("") + broadcaster.StartRecordingToSink(&v1core.EventSinkImpl{Interface: eventInterface}) + + pvw := &PVWatcher{ + provisionerName: "ibm-csi-driver", + logger: logger, + config: conf, + cloudProvider: fakeIBMCloudStorageProvider, + recorder: broadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: "pod-name"}), + } + pv := &v1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pv", + }, + Spec: v1.PersistentVolumeSpec{ + StorageClassName: "test-storage-class", + PersistentVolumeReclaimPolicy: v1.PersistentVolumeReclaimDelete, + ClaimRef: &v1.ObjectReference{ + Namespace: "test-namespace", + Name: "test-pvc", + }, + Capacity: v1.ResourceList(map[v1.ResourceName]resource.Quantity{ + v1.ResourceStorage: resource.MustParse("1Gi"), + }), + + PersistentVolumeSource: v1.PersistentVolumeSource{ + CSI: &v1.CSIPersistentVolumeSource{ + Driver: "vpc-csi-driver", + VolumeHandle: "test-volumeid", + + VolumeAttributes: map[string]string{"tags": "mytag1:1,mytag2:2", ClusterIDLabel: "12345", "volumeCRN": "test-volcrn", "iops": "3000"}, + }, + }, + }, + } + pvNoTags := pv.DeepCopy() + pvNoTags.Spec.CSI.VolumeAttributes["tags"] = "" + testCases := []struct { + testCaseName string + pv *v1.PersistentVolume + tags string + }{ + { + testCaseName: "User tags- success", + pv: pv, + tags: "mytag1:1,mytag2:2", + }, + { + testCaseName: "No user tags- success", + pv: pvNoTags, + tags: "", + }, + } + for _, testcase := range testCases { + //start test http server + server = ghttp.NewServer() + server.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest(http.MethodGet, "/v3/tags"), + ghttp.RespondWith(http.StatusOK, ` + { + "items": { + } + } + `), + ), + ) + _ = os.Setenv(IbmCloudGtAPIEndpoint, server.URL()) + t.Run(testcase.testCaseName, func(t *testing.T) { + volCRN, tags := pvw.getTags(testcase.pv, logger) + expectedTagNum := 7 + if len(testcase.tags) > 0 { + expectedTagNum = 9 + } + assert.Equal(t, expectedTagNum, len(tags)) + assert.Equal(t, "test-volcrn", volCRN) + vol := pvw.getVolumeFromPV(pv, logger) + assert.Equal(t, 1, *vol.Capacity) + assert.Equal(t, "3000", *vol.Iops) + assert.Equal(t, "test-volumeid", vol.VolumeID) + assert.NotNil(t, vol.Attributes) + assert.Equal(t, "12345", vol.Attributes[strings.ToLower(ClusterIDLabel)]) + + pvw.updateVolume(testcase.pv, testcase.pv) + }) + } +} + +// GetTestLogger ... +func GetTestLogger(t *testing.T) (logger *zap.Logger, teardown func()) { + atom := zap.NewAtomicLevel() + atom.SetLevel(zap.DebugLevel) + + encoderCfg := zap.NewProductionEncoderConfig() + encoderCfg.TimeKey = "timestamp" + encoderCfg.EncodeTime = zapcore.ISO8601TimeEncoder + + buf := &bytes.Buffer{} + + logger = zap.New( + zapcore.NewCore( + zapcore.NewJSONEncoder(encoderCfg), + zapcore.AddSync(buf), + atom, + ), + zap.AddCaller(), + ) + + teardown = func() { + _ = logger.Sync() + if t.Failed() { + t.Log(buf) + } + } + return +}