Skip to content

Commit

Permalink
[Code Change] AzCopy Error Improvements (#2647)
Browse files Browse the repository at this point in the history
* initial changes for error codes

* support multiple error codes

* additional places for error code url

* e2e test for error codes

* updating tests

* update sync test
  • Loading branch information
siminsavani-msft committed May 16, 2024
1 parent a96df7b commit 43eb15e
Show file tree
Hide file tree
Showing 10 changed files with 178 additions and 6 deletions.
2 changes: 1 addition & 1 deletion cmd/copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -2027,7 +2027,7 @@ func init() {
cooked.commandString = copyHandlerUtil{}.ConstructCommandStringFromArgs()
err = cooked.process()
if err != nil {
glcm.Error("failed to perform copy command due to error: " + err.Error())
glcm.Error("failed to perform copy command due to error: " + err.Error() + getErrorCodeUrl(err))
}

if cooked.dryrunMode {
Expand Down
2 changes: 1 addition & 1 deletion cmd/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ func init() {
if err == nil {
glcm.Exit(nil, common.EExitCode.Success())
} else {
glcm.Error(err.Error())
glcm.Error(err.Error() + getErrorCodeUrl(err))
}
},
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ var lgCmd = &cobra.Command{
// the errors from adal contains \r\n in the body, get rid of them to make the error easier to look at
prettyErr := strings.Replace(err.Error(), `\r\n`, "\n", -1)
prettyErr += "\n\nNOTE: If your credential was created in the last 5 minutes, please wait a few minutes and try again."
glcm.Error("Failed to perform login command: \n" + prettyErr)
glcm.Error("Failed to perform login command: \n" + prettyErr + getErrorCodeUrl(err))
}
return nil
},
Expand Down
2 changes: 1 addition & 1 deletion cmd/remove.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ func init() {
cooked.commandString = copyHandlerUtil{}.ConstructCommandStringFromArgs()
err = cooked.process()
if err != nil {
glcm.Error("failed to perform remove command due to error: " + err.Error())
glcm.Error("failed to perform remove command due to error: " + err.Error() + getErrorCodeUrl(err))
}

if cooked.dryrunMode {
Expand Down
42 changes: 42 additions & 0 deletions cmd/responseErrorParser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package cmd

import (
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/bloberror"
"strings"
)

// errorURLs - map of error codes that currently have shorthand URLs
var errorURLs = map[bloberror.Code]string{
bloberror.InvalidOperation: "https://aka.ms/AzCopyError/InvalidOperation",
bloberror.MissingRequiredQueryParameter: "https://aka.ms/AzCopyError/MissingRequiredQueryParameter",
bloberror.InvalidHeaderValue: "https://aka.ms/AzCopyError/InvalidHeaderValue",
bloberror.InvalidAuthenticationInfo: "https://aka.ms/AzCopyError/InvalidAuthenticationInfo",
bloberror.NoAuthenticationInformation: "https://aka.ms/AzCopyError/NoAuthenticationInformation",
bloberror.AuthenticationFailed: "https://aka.ms/AzCopyError/AuthenticationFailed",
bloberror.AccountIsDisabled: "https://aka.ms/AzCopyError/AccountIsDisabled",
bloberror.ResourceNotFound: "https://aka.ms/AzCopyError/ResourceNotFound",
bloberror.ResourceTypeMismatch: "https://aka.ms/AzCopyError/ResourceTypeMismatch",
bloberror.CannotVerifyCopySource: "https://aka.ms/AzCopyError/CannotVerifyCopySource",
bloberror.ServerBusy: "https://aka.ms/AzCopyError/ServerBusy",
}

// getErrorCodeUrl - returns url string for specific error codes
func getErrorCodeUrl(err error) string {
var urls []string
for code, url := range errorURLs {
if hasCode(err, code) {
urls = append(urls, url)
}
}

if len(urls) > 0 {
return "ERROR DETAILS: " + strings.Join(urls, "; ")
}

return "" // We do not currently have a URL for this specific error code
}

// hasCode - checks if err contains blob error code
func hasCode(err error, code bloberror.Code) bool {
return strings.Contains(err.Error(), string(code))
}
4 changes: 2 additions & 2 deletions cmd/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -753,13 +753,13 @@ func init() {

cooked, err := raw.cook()
if err != nil {
glcm.Error("error parsing the input given by the user. Failed with error " + err.Error())
glcm.Error("error parsing the input given by the user. Failed with error " + err.Error() + getErrorCodeUrl(err))
}

cooked.commandString = copyHandlerUtil{}.ConstructCommandStringFromArgs()
err = cooked.process()
if err != nil {
glcm.Error("Cannot perform sync due to error: " + err.Error())
glcm.Error("Cannot perform sync due to error: " + err.Error() + getErrorCodeUrl(err))
}
if cooked.dryrunMode {
glcm.Exit(nil, common.EExitCode.Success())
Expand Down
23 changes: 23 additions & 0 deletions e2etest/newe2e_task_resourcemanagement.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,3 +220,26 @@ func ValidateErrorOutput(a Asserter, stdout AzCopyStdout, errorMsg string) {
fmt.Println(stdout.String())
a.Error("expected error message not found in azcopy output")
}

func ValidateContainsError(a Asserter, stdout AzCopyStdout, errorMsg []string) {
if dryrunner, ok := a.(DryrunAsserter); ok && dryrunner.Dryrun() {
return
}
for _, line := range stdout.RawStdout() {
if checkMultipleErrors(errorMsg, line) {
return
}
}
fmt.Println(stdout.String())
a.Error("expected error message not found in azcopy output")
}

func checkMultipleErrors(errorMsg []string, line string) bool {
for _, e := range errorMsg {
if strings.Contains(line, e) {
return true
}
}

return false
}
63 changes: 63 additions & 0 deletions e2etest/zt_newe2e_basic_functionality_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,66 @@ func (s *BasicFunctionalitySuite) Scenario_SingleFileUploadDownload_EmptySAS(svm
// Validate that the stdout contains the missing sas message
ValidateErrorOutput(svm, stdout, "Please authenticate using Microsoft Entra ID (https://aka.ms/AzCopy/AuthZ), use AzCopy login, or append a SAS token to your Azure URL.")
}

func (s *BasicFunctionalitySuite) Scenario_Sync_EmptySASErrorCodes(svm *ScenarioVariationManager) {
dstObj := CreateResource[ContainerResourceManager](svm, GetRootResource(svm, ResolveVariation(svm, []common.Location{common.ELocation.Blob(), common.ELocation.File(), common.ELocation.BlobFS()})), ResourceDefinitionContainer{}).GetObject(svm, "test", common.EEntityType.File())

// Scale up from service to object
srcObj := CreateResource[ObjectResourceManager](svm, GetRootResource(svm, ResolveVariation(svm, []common.Location{common.ELocation.Local(), common.ELocation.Blob(), common.ELocation.File(), common.ELocation.BlobFS()})), ResourceDefinitionObject{})

// no local <-> local
if srcObj.Location().IsLocal() == dstObj.Location().IsLocal() {
svm.InvalidateScenario()
return
}

stdout, _ := RunAzCopy(
svm,
AzCopyCommand{
Verb: AzCopyVerbSync,
Targets: []ResourceManager{
AzCopyTarget{srcObj, EExplicitCredentialType.PublicAuth(), CreateAzCopyTargetOptions{}},
AzCopyTarget{dstObj, EExplicitCredentialType.PublicAuth(), CreateAzCopyTargetOptions{}},
},
Flags: CopyFlags{
CopySyncCommonFlags: CopySyncCommonFlags{
Recursive: pointerTo(true),
},
},
ShouldFail: true,
})

// Validate that the stdout contains these error URLs
ValidateContainsError(svm, stdout, []string{"https://aka.ms/AzCopyError/NoAuthenticationInformation", "https://aka.ms/AzCopyError/ResourceNotFound"})
}

func (s *BasicFunctionalitySuite) Scenario_Copy_EmptySASErrorCodes(svm *ScenarioVariationManager) {
dstObj := CreateResource[ContainerResourceManager](svm, GetRootResource(svm, ResolveVariation(svm, []common.Location{common.ELocation.Local(), common.ELocation.Blob(), common.ELocation.File()})), ResourceDefinitionContainer{}).GetObject(svm, "test", common.EEntityType.File())

// Scale up from service to object
srcObj := CreateResource[ObjectResourceManager](svm, GetRootResource(svm, ResolveVariation(svm, []common.Location{common.ELocation.Blob(), common.ELocation.File()})), ResourceDefinitionObject{})

if srcObj.Location().IsLocal() == dstObj.Location().IsLocal() {
svm.InvalidateScenario()
return
}

stdout, _ := RunAzCopy(
svm,
AzCopyCommand{
Verb: AzCopyVerbCopy,
Targets: []ResourceManager{
AzCopyTarget{srcObj, EExplicitCredentialType.PublicAuth(), CreateAzCopyTargetOptions{}},
AzCopyTarget{dstObj, EExplicitCredentialType.PublicAuth(), CreateAzCopyTargetOptions{}},
},
Flags: CopyFlags{
CopySyncCommonFlags: CopySyncCommonFlags{
Recursive: pointerTo(true),
},
},
ShouldFail: true,
})

// Validate that the stdout contains these error URLs
ValidateContainsError(svm, stdout, []string{"https://aka.ms/AzCopyError/NoAuthenticationInformation", "https://aka.ms/AzCopyError/ResourceNotFound"})
}
24 changes: 24 additions & 0 deletions e2etest/zt_newe2e_list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,3 +229,27 @@ func (s *ListSuite) Scenario_ListWithVersions(svm *ScenarioVariationManager) {
expectedSummary := &cmd.AzCopyListSummary{FileCount: "4", TotalFileSize: "6.00 KiB"}
ValidateListOutput(svm, stdout, expectedObjects, expectedSummary)
}

func (s *ListSuite) Scenario_EmptySASErrorCodes(svm *ScenarioVariationManager) {
// Scale up from service to object
// TODO: update this test once File OAuth PR is merged bc current output is "azure files requires a SAS token for authentication"
srcObj := CreateResource[ObjectResourceManager](svm, GetRootResource(svm, ResolveVariation(svm, []common.Location{common.ELocation.Blob(), common.ELocation.BlobFS()})), ResourceDefinitionObject{})

stdout, _ := RunAzCopy(
svm,
AzCopyCommand{
Verb: AzCopyVerbList,
Targets: []ResourceManager{
AzCopyTarget{srcObj, EExplicitCredentialType.PublicAuth(), CreateAzCopyTargetOptions{}},
},
Flags: ListFlags{
GlobalFlags: GlobalFlags{
OutputType: to.Ptr(common.EOutputFormat.Json()),
},
},
ShouldFail: true,
})

// Validate that the stdout contains these error URLs
ValidateErrorOutput(svm, stdout, "https://aka.ms/AzCopyError/NoAuthenticationInformation")
}
20 changes: 20 additions & 0 deletions e2etest/zt_newe2e_remove_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,23 @@ func (s *RemoveSuite) Scenario_SingleFileRemoveBlobFSEncodedPath(svm *ScenarioVa
ObjectShouldExist: to.Ptr(false),
}, false)
}

func (s *RemoveSuite) Scenario_EmptySASErrorCodes(svm *ScenarioVariationManager) {
// Scale up from service to object
// File - ShareNotFound error
// BlobFS - errors out in log file, not stdout
srcObj := CreateResource[ObjectResourceManager](svm, GetRootResource(svm, ResolveVariation(svm, []common.Location{common.ELocation.Blob()})), ResourceDefinitionObject{})

stdout, _ := RunAzCopy(
svm,
AzCopyCommand{
Verb: AzCopyVerbRemove,
Targets: []ResourceManager{
AzCopyTarget{srcObj, EExplicitCredentialType.PublicAuth(), CreateAzCopyTargetOptions{}},
},
ShouldFail: true,
})

// Validate that the stdout contains these error URLs
ValidateErrorOutput(svm, stdout, "https://aka.ms/AzCopyError/NoAuthenticationInformation")
}

0 comments on commit 43eb15e

Please sign in to comment.