From 7b6279fa80f525cc25e6b18a38848c53920f2884 Mon Sep 17 00:00:00 2001 From: umagnus Date: Thu, 12 Oct 2023 08:42:01 +0000 Subject: [PATCH] add mock for azcopy ut --- hack/boilerplate/boilerplate.generatego.txt | 15 ++ hack/boilerplate/boilerplate.gomock.txt | 15 ++ hack/boilerplate/boilerplate.py | 22 ++- hack/update-mock.sh | 28 +++ pkg/azurefile/azurefile.go | 14 +- pkg/azurefile/controllerserver.go | 2 +- pkg/azurefile/controllerserver_test.go | 93 ++++++++- pkg/azurefile/utils.go | 94 --------- pkg/azurefile/utils_test.go | 93 --------- pkg/util/util.go | 116 +++++++++++ pkg/util/util_mock.go | 66 +++++++ pkg/util/util_test.go | 208 ++++++++++++++++++++ 12 files changed, 571 insertions(+), 195 deletions(-) create mode 100644 hack/boilerplate/boilerplate.generatego.txt create mode 100644 hack/boilerplate/boilerplate.gomock.txt create mode 100644 hack/update-mock.sh create mode 100644 pkg/util/util_mock.go diff --git a/hack/boilerplate/boilerplate.generatego.txt b/hack/boilerplate/boilerplate.generatego.txt new file mode 100644 index 0000000000..0926592d38 --- /dev/null +++ b/hack/boilerplate/boilerplate.generatego.txt @@ -0,0 +1,15 @@ +/* +Copyright The Kubernetes Authors. + +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. +*/ diff --git a/hack/boilerplate/boilerplate.gomock.txt b/hack/boilerplate/boilerplate.gomock.txt new file mode 100644 index 0000000000..b66f79dd41 --- /dev/null +++ b/hack/boilerplate/boilerplate.gomock.txt @@ -0,0 +1,15 @@ +// /* +// Copyright The Kubernetes Authors. +// +// 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. +// */ diff --git a/hack/boilerplate/boilerplate.py b/hack/boilerplate/boilerplate.py index 5db7f571d6..f6b323bc32 100755 --- a/hack/boilerplate/boilerplate.py +++ b/hack/boilerplate/boilerplate.py @@ -65,6 +65,14 @@ def get_refs(): return refs +def is_generated_file(filename, data, regexs): + for d in skipped_ungenerated_files: + if d in filename: + return False + + p = regexs["generated"] + return p.search(data) + def file_passes(filename, refs, regexs): try: f = open(filename, 'r') @@ -75,15 +83,21 @@ def file_passes(filename, refs, regexs): data = f.read() f.close() + # determine if the file is automatically generated + generated = is_generated_file(filename, data, regexs) + basename = os.path.basename(filename) extension = file_extension(filename) + if generated: + if extension == "go": + extension = "gomock" if extension != "": ref = refs[extension] else: ref = refs[basename] # remove build tags from the top of Go files - if extension == "go": + if extension == "go" or extension == "gomock": p = regexs["go_build_constraints"] (data, found) = p.subn("", data, 1) @@ -136,6 +150,10 @@ def file_extension(filename): 'cluster/env.sh', 'vendor', 'test/e2e/generated/bindata.go', 'repo-infra/verify/boilerplate/test', '.glide'] + # list all the files contain 'DO NOT EDIT', but are not generated +skipped_ungenerated_files = [ + 'hack/boilerplate/boilerplate.py'] + def normalize_files(files): newfiles = [] for pathname in files: @@ -183,6 +201,8 @@ def get_regexs(): regexs["go_build_constraints"] = re.compile(r"^(//(go:build| \+build).*\n)+\n", re.MULTILINE) # strip #!.* from shell scripts regexs["shebang"] = re.compile(r"^(#!.*\n)\n*", re.MULTILINE) + # Search for generated files + regexs["generated"] = re.compile('DO NOT EDIT') return regexs diff --git a/hack/update-mock.sh b/hack/update-mock.sh new file mode 100644 index 0000000000..29010a696a --- /dev/null +++ b/hack/update-mock.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +# Copyright 2020 The Kubernetes Authors. +# +# 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. + +set -euo pipefail + +REPO_ROOT=$(realpath $(dirname ${BASH_SOURCE})/..) +COPYRIGHT_FILE="${REPO_ROOT}/hack/boilerplate/boilerplate.generatego.txt" + +if ! type mockgen &> /dev/null; then + echo "mockgen not exist, install it" + go install github.com/golang/mock/mockgen@v1.6.0 +fi + +echo "Updating mocks for util.go" +mockgen -copyright_file=$COPYRIGHT_FILE -source=pkg/util/util.go -package=util -destination=pkg/util/util_mock.go diff --git a/pkg/azurefile/azurefile.go b/pkg/azurefile/azurefile.go index 1c5ef5b063..a8d0d141e3 100644 --- a/pkg/azurefile/azurefile.go +++ b/pkg/azurefile/azurefile.go @@ -46,6 +46,7 @@ import ( csicommon "sigs.k8s.io/azurefile-csi-driver/pkg/csi-common" "sigs.k8s.io/azurefile-csi-driver/pkg/mounter" + fileutil "sigs.k8s.io/azurefile-csi-driver/pkg/util" "sigs.k8s.io/cloud-provider-azure/pkg/azureclients/fileclient" azcache "sigs.k8s.io/cloud-provider-azure/pkg/cache" azure "sigs.k8s.io/cloud-provider-azure/pkg/provider" @@ -269,6 +270,8 @@ type Driver struct { volStatsCache azcache.Resource // sas expiry time for azcopy in volume clone sasTokenExpirationMinutes int + // azcopy for provide exec mock for ut + azcopy *fileutil.Azcopy } // NewDriver Creates a NewCSIDriver object. Assumes vendor version is equal to driver version & @@ -300,6 +303,7 @@ func NewDriver(options *DriverOptions) *Driver { driver.volLockMap = newLockMap() driver.subnetLockMap = newLockMap() driver.volumeLocks = newVolumeLocks() + driver.azcopy = &fileutil.Azcopy{} var err error getter := func(key string) (interface{}, error) { return nil, nil } @@ -928,21 +932,21 @@ func (d *Driver) copyFileShare(ctx context.Context, req *csi.CreateVolumeRequest srcPath := fmt.Sprintf("https://%s.file.%s/%s%s", accountName, storageEndpointSuffix, srcFileShareName, accountSasToken) dstPath := fmt.Sprintf("https://%s.file.%s/%s%s", accountName, storageEndpointSuffix, dstFileShareName, accountSasToken) - jobState, percent, err := getAzcopyJob(dstFileShareName) + jobState, percent, err := d.azcopy.GetAzcopyJob(dstFileShareName) klog.V(2).Infof("azcopy job status: %s, copy percent: %s%%, error: %v", jobState, percent, err) - if jobState == AzcopyJobError || jobState == AzcopyJobCompleted { + if jobState == fileutil.AzcopyJobError || jobState == fileutil.AzcopyJobCompleted { return err } klog.V(2).Infof("begin to copy fileshare %s to %s", srcFileShareName, dstFileShareName) for { select { case <-timeTick: - jobState, percent, err := getAzcopyJob(dstFileShareName) + jobState, percent, err := d.azcopy.GetAzcopyJob(dstFileShareName) klog.V(2).Infof("azcopy job status: %s, copy percent: %s%%, error: %v", jobState, percent, err) switch jobState { - case AzcopyJobError, AzcopyJobCompleted: + case fileutil.AzcopyJobError, fileutil.AzcopyJobCompleted: return err - case AzcopyJobNotFound: + case fileutil.AzcopyJobNotFound: klog.V(2).Infof("copy fileshare %s to %s", srcFileShareName, dstFileShareName) out, copyErr := exec.Command("azcopy", "copy", srcPath, dstPath, "--recursive", "--check-length=false").CombinedOutput() if copyErr != nil { diff --git a/pkg/azurefile/controllerserver.go b/pkg/azurefile/controllerserver.go index 4621111458..cfae5c3e06 100644 --- a/pkg/azurefile/controllerserver.go +++ b/pkg/azurefile/controllerserver.go @@ -108,7 +108,7 @@ func (d *Driver) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) if acquired := d.volumeLocks.TryAcquire(volName); !acquired { // logging the job status if it's volume cloning if req.GetVolumeContentSource() != nil { - jobState, percent, err := getAzcopyJob(volName) + jobState, percent, err := d.azcopy.GetAzcopyJob(volName) klog.V(2).Infof("azcopy job status: %s, copy percent: %s%%, error: %v", jobState, percent, err) } return nil, status.Errorf(codes.Aborted, volumeOperationAlreadyExistsFmt, volName) diff --git a/pkg/azurefile/controllerserver_test.go b/pkg/azurefile/controllerserver_test.go index b79a4e62f6..5d95c1339e 100644 --- a/pkg/azurefile/controllerserver_test.go +++ b/pkg/azurefile/controllerserver_test.go @@ -23,12 +23,14 @@ import ( "net/http" "net/url" "reflect" - "sigs.k8s.io/cloud-provider-azure/pkg/azureclients/fileclient" "strings" "sync" "testing" "time" + "sigs.k8s.io/azurefile-csi-driver/pkg/util" + "sigs.k8s.io/cloud-provider-azure/pkg/azureclients/fileclient" + "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2022-07-01/network" "sigs.k8s.io/cloud-provider-azure/pkg/azureclients/subnetclient/mocksubnetclient" azcache "sigs.k8s.io/cloud-provider-azure/pkg/cache" @@ -1783,6 +1785,95 @@ func TestCopyVolume(t *testing.T) { } }, }, + { + name: "azcopy job is already completed", + testFunc: func(t *testing.T) { + d := NewFakeDriver() + mp := map[string]string{} + + volumeSource := &csi.VolumeContentSource_VolumeSource{ + VolumeId: "vol_1#f5713de20cde511e8ba4900#fileshare#", + } + volumeContentSourceVolumeSource := &csi.VolumeContentSource_Volume{ + Volume: volumeSource, + } + volumecontensource := csi.VolumeContentSource{ + Type: volumeContentSourceVolumeSource, + } + + req := &csi.CreateVolumeRequest{ + Name: "unit-test", + VolumeCapabilities: stdVolCap, + Parameters: mp, + VolumeContentSource: &volumecontensource, + } + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + m := util.NewMockEXEC(ctrl) + listStr := "JobId: ed1c3833-eaff-fe42-71d7-513fb065a9d9\nStart Time: Monday, 07-Aug-23 03:29:54 UTC\nStatus: Completed\nCommand: copy https://{accountName}.file.core.windows.net/{srcFileshare}{SAStoken} https://{accountName}.file.core.windows.net/{dstFileshare}{SAStoken} --recursive --check-length=false" + m.EXPECT().RunCommand(gomock.Eq("azcopy jobs list | grep dstFileshare -B 3")).Return(listStr, nil) + // if test.enableShow { + // m.EXPECT().RunCommand(gomock.Not("azcopy jobs list | grep dstContainer -B 3")).Return(test.showStr, test.showErr) + // } + + d.azcopy.ExecCmd = m + + ctx := context.Background() + + var expectedErr error + err := d.copyVolume(ctx, req, "", &fileclient.ShareOptions{Name: "dstFileshare"}, "core.windows.net") + if !reflect.DeepEqual(err, expectedErr) { + t.Errorf("Unexpected error: %v", err) + } + }, + }, + { + name: "azcopy job is first in progress and then be completed", + testFunc: func(t *testing.T) { + d := NewFakeDriver() + mp := map[string]string{} + + volumeSource := &csi.VolumeContentSource_VolumeSource{ + VolumeId: "vol_1#f5713de20cde511e8ba4900#fileshare#", + } + volumeContentSourceVolumeSource := &csi.VolumeContentSource_Volume{ + Volume: volumeSource, + } + volumecontensource := csi.VolumeContentSource{ + Type: volumeContentSourceVolumeSource, + } + + req := &csi.CreateVolumeRequest{ + Name: "unit-test", + VolumeCapabilities: stdVolCap, + Parameters: mp, + VolumeContentSource: &volumecontensource, + } + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + m := util.NewMockEXEC(ctrl) + listStr1 := "JobId: ed1c3833-eaff-fe42-71d7-513fb065a9d9\nStart Time: Monday, 07-Aug-23 03:29:54 UTC\nStatus: InProgress\nCommand: copy https://{accountName}.file.core.windows.net/{srcFileshare}{SAStoken} https://{accountName}.file.core.windows.net/{dstFileshare}{SAStoken} --recursive --check-length=false" + listStr2 := "JobId: ed1c3833-eaff-fe42-71d7-513fb065a9d9\nStart Time: Monday, 07-Aug-23 03:29:54 UTC\nStatus: Completed\nCommand: copy https://{accountName}.file.core.windows.net/{srcFileshare}{SAStoken} https://{accountName}.file.core.windows.net/{dstFileshare}{SAStoken} --recursive --check-length=false" + o1 := m.EXPECT().RunCommand(gomock.Eq("azcopy jobs list | grep dstFileshare -B 3")).Return(listStr1, nil).Times(1) + m.EXPECT().RunCommand(gomock.Not("azcopy jobs list | grep dstFileshare -B 3")).Return("Percent Complete (approx): 50.0", nil) + o2 := m.EXPECT().RunCommand(gomock.Eq("azcopy jobs list | grep dstFileshare -B 3")).Return(listStr2, nil) + gomock.InOrder(o1, o2) + + d.azcopy.ExecCmd = m + + ctx := context.Background() + + var expectedErr error + err := d.copyVolume(ctx, req, "", &fileclient.ShareOptions{Name: "dstFileshare"}, "core.windows.net") + if !reflect.DeepEqual(err, expectedErr) { + t.Errorf("Unexpected error: %v", err) + } + }, + }, } for _, tc := range testCases { diff --git a/pkg/azurefile/utils.go b/pkg/azurefile/utils.go index e6255931d4..abad8259c6 100644 --- a/pkg/azurefile/utils.go +++ b/pkg/azurefile/utils.go @@ -19,7 +19,6 @@ package azurefile import ( "fmt" "os" - "os/exec" "strconv" "strings" "sync" @@ -35,15 +34,6 @@ const ( tagKeyValueDelimiter = "=" ) -type AzcopyJobState string - -const ( - AzcopyJobError AzcopyJobState = "Error" - AzcopyJobNotFound AzcopyJobState = "NotFound" - AzcopyJobRunning AzcopyJobState = "Running" - AzcopyJobCompleted AzcopyJobState = "Completed" -) - // lockMap used to lock on entries type lockMap struct { sync.Mutex @@ -275,87 +265,3 @@ func replaceWithMap(str string, m map[string]string) string { } return str } - -// getAzcopyJob get the azcopy job status if job existed -func getAzcopyJob(dstFileshare string) (AzcopyJobState, string, error) { - cmdStr := fmt.Sprintf("azcopy jobs list | grep %s -B 3", dstFileshare) - // cmd output example: - // JobId: ed1c3833-eaff-fe42-71d7-513fb065a9d9 - // Start Time: Monday, 07-Aug-23 03:29:54 UTC - // Status: Completed (or Cancelled, InProgress) - // Command: copy https://{accountName}.file.core.windows.net/{srcFileshare}{SAStoken} https://{accountName}.file.core.windows.net/{dstFileshare}{SAStoken} --recursive --check-length=false - // -- - // JobId: b598cce3-9aa9-9640-7793-c2bf3c385a9a - // Start Time: Wednesday, 09-Aug-23 09:09:03 UTC - // Status: Cancelled - // Command: copy https://{accountName}.file.core.windows.net/{srcFileshare}{SAStoken} https://{accountName}.file.core.windows.net/{dstFileshare}{SAStoken} --recursive --check-length=false - out, err := exec.Command("sh", "-c", cmdStr).CombinedOutput() - // if grep command returns nothing, the exec will return exit status 1 error, so filter this error - if err != nil && err.Error() != "exit status 1" { - klog.Warningf("failed to get azcopy job with error: %v, jobState: %v", err, AzcopyJobError) - return AzcopyJobError, "", fmt.Errorf("couldn't list jobs in azcopy %v", err) - } - jobid, jobState, err := parseAzcopyJobList(string(out), dstFileshare) - if err != nil || jobState == AzcopyJobError { - klog.Warningf("failed to get azcopy job with error: %v, jobState: %v", err, jobState) - return AzcopyJobError, "", fmt.Errorf("couldn't parse azcopy job list in azcopy %v", err) - } - if jobState == AzcopyJobCompleted { - return jobState, "100.0", err - } - if jobid == "" { - return jobState, "", err - } - cmdPercentStr := fmt.Sprintf("azcopy jobs show %s | grep Percent", jobid) - // cmd out example: - // Percent Complete (approx): 100.0 - summary, err := exec.Command("sh", "-c", cmdPercentStr).CombinedOutput() - if err != nil { - klog.Warningf("failed to get azcopy job with error: %v, jobState: %v", err, AzcopyJobError) - return AzcopyJobError, "", fmt.Errorf("couldn't show jobs summary in azcopy %v", err) - } - jobState, percent, err := parseAzcopyJobShow(string(summary)) - if err != nil || jobState == AzcopyJobError { - klog.Warningf("failed to get azcopy job with error: %v, jobState: %v", err, jobState) - return AzcopyJobError, "", fmt.Errorf("couldn't parse azcopy job show in azcopy %v", err) - } - return jobState, percent, nil -} - -func parseAzcopyJobList(joblist string, dstFileShareName string) (string, AzcopyJobState, error) { - jobid := "" - jobSegments := strings.Split(joblist, "JobId: ") - if len(jobSegments) < 2 { - return jobid, AzcopyJobNotFound, nil - } - jobSegments = jobSegments[1:] - for _, job := range jobSegments { - segments := strings.Split(job, "\n") - if len(segments) < 4 { - return jobid, AzcopyJobError, fmt.Errorf("error parsing jobs list: %s", job) - } - statusSegments := strings.Split(segments[2], ": ") - if len(statusSegments) < 2 { - return jobid, AzcopyJobError, fmt.Errorf("error parsing jobs list status: %s", segments[2]) - } - status := statusSegments[1] - switch status { - case "InProgress": - jobid = segments[0] - case "Completed": - return jobid, AzcopyJobCompleted, nil - } - } - if jobid == "" { - return jobid, AzcopyJobNotFound, nil - } - return jobid, AzcopyJobRunning, nil -} - -func parseAzcopyJobShow(jobshow string) (AzcopyJobState, string, error) { - segments := strings.Split(jobshow, ": ") - if len(segments) < 2 { - return AzcopyJobError, "", fmt.Errorf("error parsing jobs summary: %s in Percent Complete (approx)", jobshow) - } - return AzcopyJobRunning, strings.ReplaceAll(segments[1], "\n", ""), nil -} diff --git a/pkg/azurefile/utils_test.go b/pkg/azurefile/utils_test.go index 6cd996e589..7d24ab9d28 100644 --- a/pkg/azurefile/utils_test.go +++ b/pkg/azurefile/utils_test.go @@ -589,96 +589,3 @@ func TestReplaceWithMap(t *testing.T) { } } } - -func TestParseAzcopyJobList(t *testing.T) { - tests := []struct { - desc string - str string - expectedJobid string - expectedJobState AzcopyJobState - expectedErr error - }{ - { - desc: "azcopy job not found", - str: "", - expectedJobid: "", - expectedJobState: AzcopyJobNotFound, - expectedErr: nil, - }, - { - desc: "parse azcopy job list error", - str: "JobId: ed1c3833-eaff-fe42-71d7-513fb065a9d9\nStart Time: Monday, 07-Aug-23 03:29:54 UTC", - expectedJobid: "", - expectedJobState: AzcopyJobError, - expectedErr: fmt.Errorf("error parsing jobs list: ed1c3833-eaff-fe42-71d7-513fb065a9d9\nStart Time: Monday, 07-Aug-23 03:29:54 UTC"), - }, - { - desc: "parse azcopy job list status error", - str: "JobId: ed1c3833-eaff-fe42-71d7-513fb065a9d9\nStart Time: Monday, 07-Aug-23 03:29:54 UTC\nStatus Cancelled\nCommand: copy https://{accountName}.file.core.windows.net/{srcFileshare}{SAStoken} https://{accountName}.file.core.windows.net/{dstFileshare}{SAStoken} --recursive --check-length=false", - expectedJobid: "", - expectedJobState: AzcopyJobError, - expectedErr: fmt.Errorf("error parsing jobs list status: Status Cancelled"), - }, - { - desc: "parse azcopy job not found jobid when Status is Canceled", - str: "JobId: ed1c3833-eaff-fe42-71d7-513fb065a9d9\nStart Time: Monday, 07-Aug-23 03:29:54 UTC\nStatus: Cancelled\nCommand: copy https://{accountName}.file.core.windows.net/{srcFileshare}{SAStoken} https://{accountName}.file.core.windows.net/{dstFileshare}{SAStoken} --recursive --check-length=false", - expectedJobid: "", - expectedJobState: AzcopyJobNotFound, - expectedErr: nil, - }, - { - desc: "parse azcopy job Completed", - str: "JobId: ed1c3833-eaff-fe42-71d7-513fb065a9d9\nStart Time: Monday, 07-Aug-23 03:29:54 UTC\nStatus: Completed\nCommand: copy https://{accountName}.file.core.windows.net/{srcFileshare}{SAStoken} https://{accountName}.file.core.windows.net/{dstFileshare}{SAStoken} --recursive --check-length=false", - expectedJobid: "", - expectedJobState: AzcopyJobCompleted, - expectedErr: nil, - }, - { - desc: "parse azcopy job InProgress", - str: "JobId: ed1c3833-eaff-fe42-71d7-513fb065a9d9\nStart Time: Monday, 07-Aug-23 03:29:54 UTC\nStatus: InProgress\nCommand: copy https://{accountName}.file.core.windows.net/{srcFileshare}{SAStoken} https://{accountName}.file.core.windows.net/{dstFileshare}{SAStoken} --recursive --check-length=false", - expectedJobid: "ed1c3833-eaff-fe42-71d7-513fb065a9d9", - expectedJobState: AzcopyJobRunning, - expectedErr: nil, - }, - } - - for _, test := range tests { - dstFileShare := "dstFileShare" - jobid, jobState, err := parseAzcopyJobList(test.str, dstFileShare) - if jobid != test.expectedJobid || jobState != test.expectedJobState || !reflect.DeepEqual(err, test.expectedErr) { - t.Errorf("test[%s]: unexpected jobid: %v, jobState: %v, err: %v, expected jobid: %v, jobState: %v, err: %v", test.desc, jobid, jobState, err, test.expectedJobid, test.expectedJobState, test.expectedErr) - } - } -} - -func TestParseAzcopyJobShow(t *testing.T) { - tests := []struct { - desc string - str string - expectedJobState AzcopyJobState - expectedPercent string - expectedErr error - }{ - { - desc: "error parse azcopy job show", - str: "", - expectedJobState: AzcopyJobError, - expectedPercent: "", - expectedErr: fmt.Errorf("error parsing jobs summary: in Percent Complete (approx)"), - }, - { - desc: "parse azcopy job show succeed", - str: "Percent Complete (approx): 50.0", - expectedJobState: AzcopyJobRunning, - expectedPercent: "50.0", - expectedErr: nil, - }, - } - - for _, test := range tests { - jobState, percent, err := parseAzcopyJobShow(test.str) - if jobState != test.expectedJobState || percent != test.expectedPercent || !reflect.DeepEqual(err, test.expectedErr) { - t.Errorf("test[%s]: unexpected jobState: %v, percent: %v, err: %v, expected jobState: %v, percent: %v, err: %v", test.desc, jobState, percent, err, test.expectedJobState, test.expectedPercent, test.expectedErr) - } - } -} diff --git a/pkg/util/util.go b/pkg/util/util.go index 234b18e4cb..34936d8e8c 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -17,8 +17,10 @@ limitations under the License. package util import ( + "fmt" "os" "os/exec" + "strings" "sync" "k8s.io/klog/v2" @@ -29,6 +31,15 @@ const ( MaxPathLengthWindows = 260 ) +type AzcopyJobState string + +const ( + AzcopyJobError AzcopyJobState = "Error" + AzcopyJobNotFound AzcopyJobState = "NotFound" + AzcopyJobRunning AzcopyJobState = "Running" + AzcopyJobCompleted AzcopyJobState = "Completed" +) + var mutex = &sync.Mutex{} // RoundUpBytes rounds up the volume size in bytes up to multiplications of GiB @@ -76,3 +87,108 @@ func RunPowershellCmd(command string, envs ...string) ([]byte, error) { klog.V(8).Infof("Executing command: %q", cmd.String()) return cmd.CombinedOutput() } + +type EXEC interface { + RunCommand(string) (string, error) +} + +type ExecCommand struct { +} + +func (ec *ExecCommand) RunCommand(cmd string) (string, error) { + out, err := exec.Command("sh", "-c", cmd).CombinedOutput() + return string(out), err +} + +type Azcopy struct { + ExecCmd EXEC +} + +// GetAzcopyJob get the azcopy job status if job existed +func (ac *Azcopy) GetAzcopyJob(dstFileshare string) (AzcopyJobState, string, error) { + cmdStr := fmt.Sprintf("azcopy jobs list | grep %s -B 3", dstFileshare) + // cmd output example: + // JobId: ed1c3833-eaff-fe42-71d7-513fb065a9d9 + // Start Time: Monday, 07-Aug-23 03:29:54 UTC + // Status: Completed (or Cancelled, InProgress) + // Command: copy https://{accountName}.file.core.windows.net/{srcFileshare}{SAStoken} https://{accountName}.file.core.windows.net/{dstFileshare}{SAStoken} --recursive --check-length=false + // -- + // JobId: b598cce3-9aa9-9640-7793-c2bf3c385a9a + // Start Time: Wednesday, 09-Aug-23 09:09:03 UTC + // Status: Cancelled + // Command: copy https://{accountName}.file.core.windows.net/{srcFileshare}{SAStoken} https://{accountName}.file.core.windows.net/{dstFileshare}{SAStoken} --recursive --check-length=false + if ac.ExecCmd == nil { + ac.ExecCmd = &ExecCommand{} + } + out, err := ac.ExecCmd.RunCommand(cmdStr) + // if grep command returns nothing, the exec will return exit status 1 error, so filter this error + if err != nil && err.Error() != "exit status 1" { + klog.Warningf("failed to get azcopy job with error: %v, jobState: %v", err, AzcopyJobError) + return AzcopyJobError, "", fmt.Errorf("couldn't list jobs in azcopy %v", err) + } + jobid, jobState, err := parseAzcopyJobList(out, dstFileshare) + if err != nil || jobState == AzcopyJobError { + klog.Warningf("failed to get azcopy job with error: %v, jobState: %v", err, jobState) + return AzcopyJobError, "", fmt.Errorf("couldn't parse azcopy job list in azcopy %v", err) + } + if jobState == AzcopyJobCompleted { + return jobState, "100.0", err + } + if jobid == "" { + return jobState, "", err + } + cmdPercentStr := fmt.Sprintf("azcopy jobs show %s | grep Percent", jobid) + // cmd out example: + // Percent Complete (approx): 100.0 + summary, err := ac.ExecCmd.RunCommand(cmdPercentStr) + if err != nil { + klog.Warningf("failed to get azcopy job with error: %v, jobState: %v", err, AzcopyJobError) + return AzcopyJobError, "", fmt.Errorf("couldn't show jobs summary in azcopy %v", err) + } + jobState, percent, err := parseAzcopyJobShow(summary) + if err != nil || jobState == AzcopyJobError { + klog.Warningf("failed to get azcopy job with error: %v, jobState: %v", err, jobState) + return AzcopyJobError, "", fmt.Errorf("couldn't parse azcopy job show in azcopy %v", err) + } + return jobState, percent, nil +} + +// parseAzcopyJobList parse command azcopy jobs list, get jobid and state from joblist containing dstFileShareName +func parseAzcopyJobList(joblist string, dstFileShareName string) (string, AzcopyJobState, error) { + jobid := "" + jobSegments := strings.Split(joblist, "JobId: ") + if len(jobSegments) < 2 { + return jobid, AzcopyJobNotFound, nil + } + jobSegments = jobSegments[1:] + for _, job := range jobSegments { + segments := strings.Split(job, "\n") + if len(segments) < 4 { + return jobid, AzcopyJobError, fmt.Errorf("error parsing jobs list: %s", job) + } + statusSegments := strings.Split(segments[2], ": ") + if len(statusSegments) < 2 { + return jobid, AzcopyJobError, fmt.Errorf("error parsing jobs list status: %s", segments[2]) + } + status := statusSegments[1] + switch status { + case "InProgress": + jobid = segments[0] + case "Completed": + return jobid, AzcopyJobCompleted, nil + } + } + if jobid == "" { + return jobid, AzcopyJobNotFound, nil + } + return jobid, AzcopyJobRunning, nil +} + +// parseAzcopyJobShow parse command azcopy jobs show jobid, get job state and copy percent +func parseAzcopyJobShow(jobshow string) (AzcopyJobState, string, error) { + segments := strings.Split(jobshow, ": ") + if len(segments) < 2 { + return AzcopyJobError, "", fmt.Errorf("error parsing jobs summary: %s in Percent Complete (approx)", jobshow) + } + return AzcopyJobRunning, strings.ReplaceAll(segments[1], "\n", ""), nil +} diff --git a/pkg/util/util_mock.go b/pkg/util/util_mock.go new file mode 100644 index 0000000000..f381ec968d --- /dev/null +++ b/pkg/util/util_mock.go @@ -0,0 +1,66 @@ +// /* +// Copyright The Kubernetes Authors. +// +// 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. +// */ +// + +// Code generated by MockGen. DO NOT EDIT. +// Source: pkg/util/util.go + +// Package util is a generated GoMock package. +package util + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockEXEC is a mock of EXEC interface. +type MockEXEC struct { + ctrl *gomock.Controller + recorder *MockEXECMockRecorder +} + +// MockEXECMockRecorder is the mock recorder for MockEXEC. +type MockEXECMockRecorder struct { + mock *MockEXEC +} + +// NewMockEXEC creates a new mock instance. +func NewMockEXEC(ctrl *gomock.Controller) *MockEXEC { + mock := &MockEXEC{ctrl: ctrl} + mock.recorder = &MockEXECMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockEXEC) EXPECT() *MockEXECMockRecorder { + return m.recorder +} + +// RunCommand mocks base method. +func (m *MockEXEC) RunCommand(arg0 string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RunCommand", arg0) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RunCommand indicates an expected call of RunCommand. +func (mr *MockEXECMockRecorder) RunCommand(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunCommand", reflect.TypeOf((*MockEXEC)(nil).RunCommand), arg0) +} diff --git a/pkg/util/util_test.go b/pkg/util/util_test.go index a54d259245..56354f90f9 100644 --- a/pkg/util/util_test.go +++ b/pkg/util/util_test.go @@ -17,7 +17,11 @@ limitations under the License. package util import ( + "fmt" + "reflect" "testing" + + gomock "github.com/golang/mock/gomock" ) func TestRoundUpBytes(t *testing.T) { @@ -53,3 +57,207 @@ func TestGiBToBytes(t *testing.T) { t.Fatalf("Wrong result for GiBToBytes. Got: %d", actual) } } + +func TestGetAzcopyJob(t *testing.T) { + tests := []struct { + desc string + listStr string + listErr error + enableShow bool + showStr string + showErr error + expectedJobState AzcopyJobState + expectedPercent string + expectedErr error + }{ + { + desc: "run exec get error", + listStr: "", + listErr: fmt.Errorf("error"), + enableShow: false, + showStr: "", + showErr: nil, + expectedJobState: AzcopyJobError, + expectedPercent: "", + expectedErr: fmt.Errorf("couldn't list jobs in azcopy error"), + }, + { + desc: "run exec parse azcopy job list get error", + listStr: "JobId: ed1c3833-eaff-fe42-71d7-513fb065a9d9\nStart Time: Monday, 07-Aug-23 03:29:54 UTC", + listErr: nil, + enableShow: false, + showStr: "", + showErr: nil, + expectedJobState: AzcopyJobError, + expectedPercent: "", + expectedErr: fmt.Errorf("couldn't parse azcopy job list in azcopy error parsing jobs list: ed1c3833-eaff-fe42-71d7-513fb065a9d9\nStart Time: Monday, 07-Aug-23 03:29:54 UTC"), + }, + { + desc: "run exec parse azcopy job not found jobid when Status is Canceled", + listStr: "JobId: ed1c3833-eaff-fe42-71d7-513fb065a9d9\nStart Time: Monday, 07-Aug-23 03:29:54 UTC\nStatus: Cancelled\nCommand: copy https://{accountName}.file.core.windows.net/{srcFileshare}{SAStoken} https://{accountName}.file.core.windows.net/{dstFileshare}{SAStoken} --recursive --check-length=false", + listErr: nil, + enableShow: false, + showStr: "", + showErr: nil, + expectedJobState: AzcopyJobNotFound, + expectedPercent: "", + expectedErr: nil, + }, + { + desc: "run exec parse azcopy job Completed", + listStr: "JobId: ed1c3833-eaff-fe42-71d7-513fb065a9d9\nStart Time: Monday, 07-Aug-23 03:29:54 UTC\nStatus: Completed\nCommand: copy https://{accountName}.file.core.windows.net/{srcFileshare}{SAStoken} https://{accountName}.file.core.windows.net/{dstFileshare}{SAStoken} --recursive --check-length=false", + listErr: nil, + enableShow: false, + showStr: "", + showErr: nil, + expectedJobState: AzcopyJobCompleted, + expectedPercent: "100.0", + expectedErr: nil, + }, + { + desc: "run exec get error in azcopy jobs show", + listStr: "JobId: ed1c3833-eaff-fe42-71d7-513fb065a9d9\nStart Time: Monday, 07-Aug-23 03:29:54 UTC\nStatus: InProgress\nCommand: copy https://{accountName}.file.core.windows.net/{srcFileshare}{SAStoken} https://{accountName}.file.core.windows.net/{dstFileshare}{SAStoken} --recursive --check-length=false", + listErr: nil, + enableShow: true, + showStr: "", + showErr: fmt.Errorf("error"), + expectedJobState: AzcopyJobError, + expectedPercent: "", + expectedErr: fmt.Errorf("couldn't show jobs summary in azcopy error"), + }, + { + desc: "run exec parse azcopy job show error", + listStr: "JobId: ed1c3833-eaff-fe42-71d7-513fb065a9d9\nStart Time: Monday, 07-Aug-23 03:29:54 UTC\nStatus: InProgress\nCommand: copy https://{accountName}.file.core.windows.net/{srcFileshare}{SAStoken} https://{accountName}.file.core.windows.net/{dstFileshare}{SAStoken} --recursive --check-length=false", + listErr: nil, + enableShow: true, + showStr: "", + showErr: nil, + expectedJobState: AzcopyJobError, + expectedPercent: "", + expectedErr: fmt.Errorf("couldn't parse azcopy job show in azcopy error parsing jobs summary: in Percent Complete (approx)"), + }, + { + desc: "run exec parse azcopy job show succeed", + listStr: "JobId: ed1c3833-eaff-fe42-71d7-513fb065a9d9\nStart Time: Monday, 07-Aug-23 03:29:54 UTC\nStatus: InProgress\nCommand: copy https://{accountName}.file.core.windows.net/{srcFileshare}{SAStoken} https://{accountName}.file.core.windows.net/{dstFileshare}{SAStoken} --recursive --check-length=false", + listErr: nil, + enableShow: true, + showStr: "Percent Complete (approx): 50.0", + showErr: nil, + expectedJobState: AzcopyJobRunning, + expectedPercent: "50.0", + expectedErr: nil, + }, + } + for _, test := range tests { + dstFileshare := "dstFileshare" + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + m := NewMockEXEC(ctrl) + m.EXPECT().RunCommand(gomock.Eq("azcopy jobs list | grep dstFileshare -B 3")).Return(test.listStr, test.listErr) + if test.enableShow { + m.EXPECT().RunCommand(gomock.Not("azcopy jobs list | grep dstFileshare -B 3")).Return(test.showStr, test.showErr) + } + + azcopyFunc := &Azcopy{} + azcopyFunc.ExecCmd = m + jobState, percent, err := azcopyFunc.GetAzcopyJob(dstFileshare) + if jobState != test.expectedJobState || percent != test.expectedPercent || !reflect.DeepEqual(err, test.expectedErr) { + t.Errorf("test[%s]: unexpected jobState: %v, percent: %v, err: %v, expected jobState: %v, percent: %v, err: %v", test.desc, jobState, percent, err, test.expectedJobState, test.expectedPercent, test.expectedErr) + } + } +} + +func TestParseAzcopyJobList(t *testing.T) { + tests := []struct { + desc string + str string + expectedJobid string + expectedJobState AzcopyJobState + expectedErr error + }{ + { + desc: "azcopy job not found", + str: "", + expectedJobid: "", + expectedJobState: AzcopyJobNotFound, + expectedErr: nil, + }, + { + desc: "parse azcopy job list error", + str: "JobId: ed1c3833-eaff-fe42-71d7-513fb065a9d9\nStart Time: Monday, 07-Aug-23 03:29:54 UTC", + expectedJobid: "", + expectedJobState: AzcopyJobError, + expectedErr: fmt.Errorf("error parsing jobs list: ed1c3833-eaff-fe42-71d7-513fb065a9d9\nStart Time: Monday, 07-Aug-23 03:29:54 UTC"), + }, + { + desc: "parse azcopy job list status error", + str: "JobId: ed1c3833-eaff-fe42-71d7-513fb065a9d9\nStart Time: Monday, 07-Aug-23 03:29:54 UTC\nStatus Cancelled\nCommand: copy https://{accountName}.file.core.windows.net/{srcFileshare}{SAStoken} https://{accountName}.file.core.windows.net/{dstFileshare}{SAStoken} --recursive --check-length=false", + expectedJobid: "", + expectedJobState: AzcopyJobError, + expectedErr: fmt.Errorf("error parsing jobs list status: Status Cancelled"), + }, + { + desc: "parse azcopy job not found jobid when Status is Canceled", + str: "JobId: ed1c3833-eaff-fe42-71d7-513fb065a9d9\nStart Time: Monday, 07-Aug-23 03:29:54 UTC\nStatus: Cancelled\nCommand: copy https://{accountName}.file.core.windows.net/{srcFileshare}{SAStoken} https://{accountName}.file.core.windows.net/{dstFileshare}{SAStoken} --recursive --check-length=false", + expectedJobid: "", + expectedJobState: AzcopyJobNotFound, + expectedErr: nil, + }, + { + desc: "parse azcopy job Completed", + str: "JobId: ed1c3833-eaff-fe42-71d7-513fb065a9d9\nStart Time: Monday, 07-Aug-23 03:29:54 UTC\nStatus: Completed\nCommand: copy https://{accountName}.file.core.windows.net/{srcFileshare}{SAStoken} https://{accountName}.file.core.windows.net/{dstFileshare}{SAStoken} --recursive --check-length=false", + expectedJobid: "", + expectedJobState: AzcopyJobCompleted, + expectedErr: nil, + }, + { + desc: "parse azcopy job InProgress", + str: "JobId: ed1c3833-eaff-fe42-71d7-513fb065a9d9\nStart Time: Monday, 07-Aug-23 03:29:54 UTC\nStatus: InProgress\nCommand: copy https://{accountName}.file.core.windows.net/{srcFileshare}{SAStoken} https://{accountName}.file.core.windows.net/{dstFileshare}{SAStoken} --recursive --check-length=false", + expectedJobid: "ed1c3833-eaff-fe42-71d7-513fb065a9d9", + expectedJobState: AzcopyJobRunning, + expectedErr: nil, + }, + } + + for _, test := range tests { + dstFileShare := "dstFileShare" + jobid, jobState, err := parseAzcopyJobList(test.str, dstFileShare) + if jobid != test.expectedJobid || jobState != test.expectedJobState || !reflect.DeepEqual(err, test.expectedErr) { + t.Errorf("test[%s]: unexpected jobid: %v, jobState: %v, err: %v, expected jobid: %v, jobState: %v, err: %v", test.desc, jobid, jobState, err, test.expectedJobid, test.expectedJobState, test.expectedErr) + } + } +} + +func TestParseAzcopyJobShow(t *testing.T) { + tests := []struct { + desc string + str string + expectedJobState AzcopyJobState + expectedPercent string + expectedErr error + }{ + { + desc: "error parse azcopy job show", + str: "", + expectedJobState: AzcopyJobError, + expectedPercent: "", + expectedErr: fmt.Errorf("error parsing jobs summary: in Percent Complete (approx)"), + }, + { + desc: "parse azcopy job show succeed", + str: "Percent Complete (approx): 50.0", + expectedJobState: AzcopyJobRunning, + expectedPercent: "50.0", + expectedErr: nil, + }, + } + + for _, test := range tests { + jobState, percent, err := parseAzcopyJobShow(test.str) + if jobState != test.expectedJobState || percent != test.expectedPercent || !reflect.DeepEqual(err, test.expectedErr) { + t.Errorf("test[%s]: unexpected jobState: %v, percent: %v, err: %v, expected jobState: %v, percent: %v, err: %v", test.desc, jobState, percent, err, test.expectedJobState, test.expectedPercent, test.expectedErr) + } + } +}