diff --git a/cmd/copy.go b/cmd/copy.go index becfac37b..2f2f4f789 100644 --- a/cmd/copy.go +++ b/cmd/copy.go @@ -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 { diff --git a/cmd/list.go b/cmd/list.go index 0090d4550..77813fa78 100755 --- a/cmd/list.go +++ b/cmd/list.go @@ -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)) } }, } diff --git a/cmd/login.go b/cmd/login.go index 61c18a4b4..b0e284ea7 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -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 }, diff --git a/cmd/remove.go b/cmd/remove.go index 77b51e9c7..03fc0422b 100644 --- a/cmd/remove.go +++ b/cmd/remove.go @@ -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 { diff --git a/cmd/responseErrorParser.go b/cmd/responseErrorParser.go new file mode 100644 index 000000000..8612c2a3c --- /dev/null +++ b/cmd/responseErrorParser.go @@ -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)) +} diff --git a/cmd/sync.go b/cmd/sync.go index 1afd9d4a2..d356d720c 100644 --- a/cmd/sync.go +++ b/cmd/sync.go @@ -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()) diff --git a/e2etest/newe2e_task_resourcemanagement.go b/e2etest/newe2e_task_resourcemanagement.go index 02429704d..0151a4497 100644 --- a/e2etest/newe2e_task_resourcemanagement.go +++ b/e2etest/newe2e_task_resourcemanagement.go @@ -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 +} diff --git a/e2etest/zt_newe2e_basic_functionality_test.go b/e2etest/zt_newe2e_basic_functionality_test.go index cf0a58b7e..24a65a946 100644 --- a/e2etest/zt_newe2e_basic_functionality_test.go +++ b/e2etest/zt_newe2e_basic_functionality_test.go @@ -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"}) +} diff --git a/e2etest/zt_newe2e_list_test.go b/e2etest/zt_newe2e_list_test.go index 9ad0fdb65..010ffbeb4 100644 --- a/e2etest/zt_newe2e_list_test.go +++ b/e2etest/zt_newe2e_list_test.go @@ -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") +} diff --git a/e2etest/zt_newe2e_remove_test.go b/e2etest/zt_newe2e_remove_test.go index aa32ac07e..957f4c09b 100644 --- a/e2etest/zt_newe2e_remove_test.go +++ b/e2etest/zt_newe2e_remove_test.go @@ -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") +}