diff --git a/cmd/git/main.go b/cmd/git/main.go index 48ab57a04a..ab67e5cfb9 100644 --- a/cmd/git/main.go +++ b/cmd/git/main.go @@ -16,6 +16,7 @@ import ( "regexp" "strings" + shpgit "github.com/shipwright-io/build/pkg/git" "github.com/spf13/pflag" ) @@ -36,6 +37,7 @@ type ExitError struct { Code int Message string Cause error + Reason shpgit.ErrorClass } func (e ExitError) Error() string { @@ -54,6 +56,8 @@ type settings struct { secretPath string skipValidation bool gitURLRewrite bool + resultFileErrorMessage string + resultFileErrorReason string } var flagValues settings @@ -78,6 +82,10 @@ func init() { pflag.StringVar(&flagValues.resultFileBranchName, "result-file-branch-name", "", "A file to write the branch name to.") pflag.StringVar(&flagValues.secretPath, "secret-path", "", "A directory that contains a secret. Either username and password for basic authentication. Or a SSH private key and optionally a known hosts file. Optional.") + // Flags with paths for writing error related information + pflag.StringVar(&flagValues.resultFileErrorMessage, "result-file-error-message", "", "A file to write the error message to.") + pflag.StringVar(&flagValues.resultFileErrorReason, "result-file-error-reason", "", "A file to write the error reason to.") + // Optional flag to be able to override the default shallow clone depth, // which should be fine for almost all use cases we use the Git source step // for (in the context of Shipwright build). @@ -96,6 +104,10 @@ func main() { exitcode = err.Code } + if err := writeErrorResults(shpgit.NewErrorResultFromMessage(err.Error())); err != nil { + log.Printf("Could not write error results: %s", err.Error()) + } + log.Print(err.Error()) os.Exit(exitcode) } @@ -459,10 +471,18 @@ func checkCredentials() (credentialType, error) { return typePrivateKey, nil case hasPrivateKey && !isSSHGitURL: - return typeUndef, &ExitError{Code: 110, Message: "Credential/URL inconsistency: SSH credentials provided, but URL is not a SSH Git URL"} + return typeUndef, &ExitError{ + Code: 110, + Message: shpgit.AuthUnexpectedSSH.ToMessage(), + Reason: shpgit.AuthUnexpectedSSH, + } case !hasPrivateKey && isSSHGitURL: - return typeUndef, &ExitError{Code: 110, Message: "Credential/URL inconsistency: No SSH credentials provided, but URL is a SSH Git URL"} + return typeUndef, &ExitError{ + Code: 110, + Message: shpgit.AuthExpectedSSH.ToMessage(), + Reason: shpgit.AuthExpectedSSH, + } } // Checking whether mounted secret is of type `kubernetes.io/basic-auth` @@ -475,9 +495,35 @@ func checkCredentials() (credentialType, error) { return typeUsernamePassword, nil case hasUsername && !hasPassword || !hasUsername && hasPassword: - return typeUndef, &ExitError{Code: 110, Message: "Basic Auth incomplete: Both username and password need to be configured"} - + return typeUndef, &ExitError{ + Code: 110, + Message: shpgit.AuthBasicIncomplete.ToMessage(), + Reason: shpgit.AuthBasicIncomplete, + } } return typeUndef, &ExitError{Code: 110, Message: "Unsupported type of credentials provided, either SSH private key or username/password is supported"} } + +func writeErrorResults(failure *shpgit.ErrorResult) (err error) { + if flagValues.resultFileErrorReason == "" || flagValues.resultFileErrorMessage == "" { + return nil + } + + messageToWrite := failure.Message + messageLengthThreshold := 300 + + if len(messageToWrite) > messageLengthThreshold { + messageToWrite = messageToWrite[:messageLengthThreshold-3] + "..." + } + + if err = os.WriteFile(flagValues.resultFileErrorMessage, []byte(strings.TrimSpace(messageToWrite)), 0666); err != nil { + return err + } + + if err = os.WriteFile(flagValues.resultFileErrorReason, []byte(failure.Reason.String()), 0666); err != nil { + return err + } + + return nil +} diff --git a/cmd/git/main_test.go b/cmd/git/main_test.go index a5c375add4..acd4bbad48 100644 --- a/cmd/git/main_test.go +++ b/cmd/git/main_test.go @@ -17,6 +17,7 @@ import ( . "github.com/onsi/gomega" . "github.com/shipwright-io/build/cmd/git" + shpgit "github.com/shipwright-io/build/pkg/git" ) var _ = Describe("Git Resource", func() { @@ -429,4 +430,137 @@ var _ = Describe("Git Resource", func() { }) }) }) + + Context("failure diagnostics", func() { + const ( + exampleSSHGithubRepo = "git@github.com:shipwright-io/sample-go.git" + nonExistingSSHGithubRepo = "git@github.com:shipwright-io/sample-go-nonexistent.git" + exampleHTTPGithubNonExistent = "https://github.com/shipwright-io/sample-go-nonexistent.git" + githubHTTPRepo = "https://github.com/shipwright-io/sample-go.git" + + exampleSSHGitlabRepo = "git@gitlab.com:gitlab-org/gitlab-runner.git" + exampleHTTPGitlabNonExistent = "https://gitlab.com/gitlab-org/gitlab-runner-nonexistent.git" + gitlabHTTPRepo = "https://gitlab.com/gitlab-org/gitlab-runner.git" + ) + + It("should detect invalid basic auth credentials", func() { + testForRepo := func(repo string) { + withTempDir(func(secret string) { + file(filepath.Join(secret, "username"), 0400, []byte("ship")) + file(filepath.Join(secret, "password"), 0400, []byte("ghp_sFhFsSHhTzMDreGRLjmks4Tzuzgthdvfsrta")) + + withTempDir(func(target string) { + err := run( + "--url", repo, + "--secret-path", secret, + "--target", target, + ) + + Expect(err).ToNot(BeNil()) + + errorResult := shpgit.NewErrorResultFromMessage(err.Error()) + + Expect(errorResult.Reason.String()).To(Equal(shpgit.AuthInvalidUserOrPass.String())) + }) + }) + } + + testForRepo(exampleHTTPGitlabNonExistent) + testForRepo(exampleHTTPGithubNonExistent) + }) + + It("should detect invalid ssh credentials", func() { + testForRepo := func(repo string) { + withTempDir(func(target string) { + withTempDir(func(secret string) { + file(filepath.Join(secret, "ssh-privatekey"), 0400, []byte("invalid")) + err := run( + "--url", repo, + "--target", target, + "--secret-path", secret, + ) + + Expect(err).ToNot(BeNil()) + + errorResult := shpgit.NewErrorResultFromMessage(err.Error()) + + Expect(errorResult.Reason.String()).To(Equal(shpgit.AuthInvalidKey.String())) + }) + }) + } + testForRepo(exampleSSHGithubRepo) + testForRepo(exampleSSHGitlabRepo) + }) + + It("should prompt auth for non-existing or private repo", func() { + testForRepo := func(repo string) { + withTempDir(func(target string) { + err := run( + "--url", repo, + "--target", target, + ) + + Expect(err).ToNot(BeNil()) + + errorResult := shpgit.NewErrorResultFromMessage(err.Error()) + + Expect(errorResult.Reason.String()).To(Equal(shpgit.AuthPrompted.String())) + }) + } + + testForRepo(exampleHTTPGithubNonExistent) + testForRepo(exampleHTTPGitlabNonExistent) + }) + + It("should detect non-existing revision", func() { + testRepo := func(repo string) { + withTempDir(func(target string) { + err := run( + "--url", repo, + "--target", target, + "--revision", "non-existent", + ) + + Expect(err).ToNot(BeNil()) + + errorResult := shpgit.NewErrorResultFromMessage(err.Error()) + Expect(errorResult.Reason.String()).To(Equal(shpgit.RevisionNotFound.String())) + }) + } + + testRepo(githubHTTPRepo) + testRepo(gitlabHTTPRepo) + }) + + It("should detect non-existing repo given ssh authentication", func() { + sshPrivateKey := os.Getenv("TEST_GIT_PRIVATE_SSH_KEY") + if sshPrivateKey == "" { + Skip("Skipping private repository tests since TEST_GIT_PRIVATE_SSH_KEY environment variable is not set") + } + + testRepo := func(repo string) { + withTempDir(func(target string) { + withTempDir(func(secret string) { + // Mock the filesystem state of `kubernetes.io/ssh-auth` type secret volume mount + GinkgoWriter.Write([]byte(sshPrivateKey)) + file(filepath.Join(secret, "ssh-privatekey"), 0400, []byte(sshPrivateKey)) + + err := run( + "--url", repo, + "--target", target, + "--secret-path", secret, + ) + + Expect(err).ToNot(BeNil()) + + errorResult := shpgit.NewErrorResultFromMessage(err.Error()) + Expect(errorResult.Reason.String()).To(Equal(shpgit.RepositoryNotFound.String())) + }) + }) + } + + testRepo(nonExistingSSHGithubRepo) + //TODO: once gitlab credentials are available: testRepo(nonExistingSSHGitlabRepo) + }) + }) }) diff --git a/docs/buildrun.md b/docs/buildrun.md index 88bd6557ea..41be66ce9e 100644 --- a/docs/buildrun.md +++ b/docs/buildrun.md @@ -17,6 +17,7 @@ SPDX-License-Identifier: Apache-2.0 - [BuildRun Status](#buildrun-status) - [Understanding the state of a BuildRun](#understanding-the-state-of-a-buildrun) - [Understanding failed BuildRuns](#understanding-failed-buildruns) + - [Understanding failed git-source step](#understanding-failed-git-source-step) - [Step Results in BuildRun Status](#step-results-in-buildrun-status) - [Build Snapshot](#build-snapshot) - [Relationship with Tekton Tasks](#relationship-with-tekton-tasks) @@ -292,6 +293,23 @@ status: reason: GitRemotePrivate ``` +#### Understanding failed git-source step + +All git related operations support error reporting via `Status.FailureDetails`. The following table explains the possible +error reasons: + +| Reason | Description | +| --- | --- | +| `GitAuthInvalidUserOrPass` | Basic authentication has failed. Check your username or password. Note: GitHub requires a personal access token instead of your regular password. | +| `GitAuthInvalidKey` | The key is invalid for the specified target. Please make sure that the Git repository exists, you have sufficient permissions, and the key is in the right format. | +| `GitRevisionNotFound` | The remote revision does not exist. Check the revision specified in your Build. | +| `GitRemoteRepositoryNotFound`| The source repository does not exist, or you have insufficient permission to access it. | +| `GitRemoteRepositoryPrivate` | You are trying to access a non existing or private repository without having sufficient permissions to access it via HTTPS. | +| `GitBasicAuthIncomplete`| Basic Auth incomplete: Both username and password need to be configured. | +| `GitSSHAuthUnexpected`| Credential/URL inconsistency: SSH credentials provided, but URL is not a SSH Git URL. | +| `GitSSHAuthExpected`| Credential/URL inconsistency: No SSH credentials provided, but URL is a SSH Git URL. | +| `GitError` | The specific error reason is unknown. Check the error message for more information. | + ### Step Results in BuildRun Status After the successful completion of a `BuildRun`, the `.status` field contains the results (`.status.taskResults`) emitted from the `TaskRun` steps generate by the `BuildRun` controller as part of processing the `BuildRun`. These results contain valuable metadata for users, like the _image digest_ or the _commit sha_ of the source code used for building. diff --git a/pkg/git/error_parser.go b/pkg/git/error_parser.go new file mode 100644 index 0000000000..c7ce3e4a97 --- /dev/null +++ b/pkg/git/error_parser.go @@ -0,0 +1,312 @@ +// Copyright The Shipwright Contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package git + +import ( + "bufio" + "errors" + "regexp" + "strings" +) + +type ( + // ErrorClass classifies git stdout error messages in broader categories + ErrorClass int + // Prefix is part of an error message output and can be used to determine which participant in the git protocol + // send parts of the error message + Prefix int +) + +const ( + unknownPrefix Prefix = iota + remotePrefix + fatalPrefix + errorPrefix +) + +const ( + // Unknown is the class of choice if no other class fits. + Unknown ErrorClass = iota + // AuthInvalidUserOrPass expresses that basic authentication is not possible. + AuthInvalidUserOrPass + // AuthExpectedSSH expresses that the ssh protocol is used for git operations but basic auth was provided. + AuthExpectedSSH + // AuthUnexpectedSSH expresses that the https protocol is used for git operations but a ssh key was provided. + AuthUnexpectedSSH + // AuthBasicIncomplete expresses that either username or password is missing in basic auth credentials + AuthBasicIncomplete + // AuthInvalidKey expresses that ssh authentication is not possible + AuthInvalidKey + // RevisionNotFound expresses that a remote branch does not exist. + RevisionNotFound + // RepositoryNotFound expresses that the remote target for the git operation does not exist. It triggers when an + // error message is enough to determine that the remote target does not exist and is mostly derived from the + // Git server's messages e.g. GitLab or GitHub. + RepositoryNotFound + // AuthPrompted is caused when a repo is not found, is private and authentication is insufficient + AuthPrompted +) + +type rawToken struct { + raw string +} + +type prefixToken struct { + scope Prefix + rawToken +} + +type errorClassToken struct { + class ErrorClass + rawToken +} + +type errorToken struct { + prefixToken prefixToken + classToken errorClassToken +} + +// ErrorResult is a representation of a runtime error of a git operation that presents a reason and a message +type ErrorResult struct { + Message string + Reason ErrorClass +} + +func (rawToken rawToken) String() string { + return rawToken.raw +} + +func (class ErrorClass) String() string { + switch class { + case AuthInvalidUserOrPass: + return "GitAuthInvalidUserOrPass" + case AuthInvalidKey: + return "GitAuthInvalidKey" + case RevisionNotFound: + return "GitRevisionNotFound" + case RepositoryNotFound: + return "GitRemoteRepositoryNotFound" + case AuthPrompted: + return "GitRemoteRepositoryPrivate" + case AuthBasicIncomplete: + return "GitBasicAuthIncomplete" + case AuthUnexpectedSSH: + return "GitSSHAuthUnexpected" + case AuthExpectedSSH: + return "GitSSHAuthExpected" + } + + return "GitError" +} + +// ToMessage is a function that transforms an error class to an error message +func (class ErrorClass) ToMessage() string { + switch class { + case RepositoryNotFound: + return "The source repository does not exist, or you have insufficient permission to access it." + case AuthInvalidUserOrPass: + return "Basic authentication has failed. Check your username or password. Note: GitHub requires a personal access token instead of your regular password." + case AuthPrompted: + return "The source repository does not exist, or you have insufficient permission to access it." + case RevisionNotFound: + return "The remote revision does not exist. Check your revision argument." + case AuthInvalidKey: + return "The key is invalid for the specified target. Please make sure that the Git repository exists, you have sufficient permissions, and the key is in the right format." + case AuthUnexpectedSSH: + return "Credential/URL inconsistency: SSH credentials provided, but URL is not a SSH Git URL." + case AuthExpectedSSH: + return "Credential/URL inconsistency: No SSH credentials provided, but URL is a SSH Git URL." + case AuthBasicIncomplete: + return "Basic Auth incomplete: Both username and password need to be configured." + } + + return "Git encountered an unknown error." +} + +func (token errorToken) String() string { + return token.prefixToken.String() + ": " + token.classToken.String() +} + +func parse(message string) (tokenList []errorToken) { + reader := strings.NewReader(message) + scanner := bufio.NewScanner(reader) + + for scanner.Scan() { + if token, err := parseLine(scanner.Text()); err == nil { + tokenList = append(tokenList, *token) + } + } + + return tokenList +} + +var errWrongFormat = errors.New("not in the right format of 'prefix:message'") + +func parseLine(line string) (*errorToken, error) { + var ( + prefixBuilder strings.Builder + errorMessage string + ) + + for i, char := range line { + if char == ':' { + errorMessage = line[i+1:] + + break + } + + prefixBuilder.WriteRune(char) + } + + if len(strings.TrimSpace(errorMessage)) == 0 { + return nil, errWrongFormat + } + + prefixToken := parsePrefix(prefixBuilder.String()) + errorClassToken := parseErrorMessage(errorMessage) + + return &errorToken{classToken: errorClassToken, prefixToken: prefixToken}, nil +} + +func parsePrefix(raw string) prefixToken { + prefix := unknownPrefix + + switch strings.ToLower(strings.TrimSpace(raw)) { + case "fatal": + prefix = fatalPrefix + case "remote": + prefix = remotePrefix + case "error": + prefix = errorPrefix + } + + return prefixToken{prefix, rawToken{raw}} +} + +func isAuthInvalidUserOrPass(raw string) bool { + return strings.Contains(raw, "authentication failed for") || + strings.Contains(raw, "invalid username or password") +} + +func isAuthPrompted(raw string) bool { + return strings.Contains(raw, "terminal prompts disabled") +} + +func isAuthInvalidKey(raw string) bool { + return strings.Contains(raw, "could not read from remote") +} + +func isRepositoryNotFound(raw string) bool { + isMatch, _ := regexp.Match("(repository|project) .* found", []byte(raw)) + + return isMatch +} + +func isBranchNotFound(raw string) bool { + return strings.Contains(raw, "remote branch") && strings.Contains(raw, "not found") +} + +func parseErrorMessage(raw string) errorClassToken { + errorClass := Unknown + toCheck := strings.ToLower(strings.TrimSpace(raw)) + + switch { + case isAuthInvalidUserOrPass(toCheck): + errorClass = AuthInvalidUserOrPass + case isAuthPrompted(toCheck): + errorClass = AuthPrompted + case isAuthInvalidKey(toCheck): + errorClass = AuthInvalidKey + case isRepositoryNotFound(toCheck): + errorClass = RepositoryNotFound + case isBranchNotFound(toCheck): + errorClass = RevisionNotFound + } + + return errorClassToken{errorClass, rawToken{ + raw, + }} +} + +func classifyTokensWithRemotePrefix(tokens []errorToken) ErrorClass { + for _, remoteToken := range tokens { + switch remoteToken.classToken.class { + case AuthInvalidUserOrPass: + return AuthInvalidUserOrPass + case RepositoryNotFound: + return RepositoryNotFound + } + } + + return Unknown +} + +func classifyTokensWithErrorPrefix(tokens []errorToken) ErrorClass { + for _, remoteToken := range tokens { + if remoteToken.classToken.class == RepositoryNotFound { + return RepositoryNotFound + } + } + + return Unknown +} + +func classifyTokensWithFatalPrefix(tokens []errorToken) ErrorClass { + for _, fatalToken := range tokens { + switch fatalToken.classToken.class { + case AuthInvalidKey: + return AuthInvalidKey + case AuthPrompted: + return AuthPrompted + case RevisionNotFound: + return RevisionNotFound + case AuthInvalidUserOrPass: + return AuthInvalidUserOrPass + } + } + + return Unknown +} + +func classifyErrorFromTokens(tokens []errorToken) ErrorClass { + classifierMap := map[Prefix][]errorToken{} + for _, token := range tokens { + classifierMap[token.prefixToken.scope] = append(classifierMap[token.prefixToken.scope], token) + } + + if errorClass := classifyTokensWithRemotePrefix(classifierMap[remotePrefix]); errorClass != Unknown { + return errorClass + } + + if errorClass := classifyTokensWithErrorPrefix(classifierMap[errorPrefix]); errorClass != Unknown { + return errorClass + } + + if errorClass := classifyTokensWithFatalPrefix(classifierMap[fatalPrefix]); errorClass != Unknown { + return errorClass + } + + return Unknown +} + +func extractResultsFromTokens(tokens []errorToken) *ErrorResult { + mainErrorClass := classifyErrorFromTokens(tokens) + + if mainErrorClass == Unknown { + builder := strings.Builder{} + for _, token := range tokens { + builder.WriteString(token.String() + "\n") + } + + return &ErrorResult{Message: builder.String(), Reason: Unknown} + } + + return &ErrorResult{Message: mainErrorClass.ToMessage(), Reason: mainErrorClass} +} + +// NewErrorResultFromMessage parses a message, derives an error result and returns an instance of ErrorResult. +func NewErrorResultFromMessage(message string) *ErrorResult { + return extractResultsFromTokens(parse(message)) +} diff --git a/pkg/git/error_parser_test.go b/pkg/git/error_parser_test.go new file mode 100644 index 0000000000..1b15f37c05 --- /dev/null +++ b/pkg/git/error_parser_test.go @@ -0,0 +1,66 @@ +// Copyright The Shipwright Contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package git + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Parsing Git Error Messages", func() { + Context("parse raw to prefixToken", func() { + It("should recognize and parse fatal", func() { + parsed := parsePrefix("fatal") + + Expect(parsed.scope).To(Equal(fatalPrefix)) + Expect(parsed.raw).To(Equal("fatal")) + }) + It("should recognize and parse remote", func() { + parsed := parsePrefix("remote") + + Expect(parsed.scope).To(Equal(remotePrefix)) + Expect(parsed.raw).To(Equal("remote")) + }) + It("should not parse unknown input as general", func() { + parsed := parsePrefix("random") + + Expect(parsed.scope).To(Equal(unknownPrefix)) + Expect(parsed.raw).To(Equal("random")) + }) + }) + Context("Parse raw to errorToken", func() { + It("should recognize and parse unknown branch", func() { + parsed := parseErrorMessage("Remote branch not found") + Expect(parsed.class).To(Equal(RevisionNotFound)) + }) + It("should recognize and parse invalid auth key", func() { + parsed := parseErrorMessage("could not read from remote.") + Expect(parsed.class).To(Equal(AuthInvalidKey)) + }) + It("should recognize and parse invalid basic auth", func() { + parsed := parseErrorMessage("Invalid username or password.") + Expect(parsed.class).To(Equal(AuthInvalidUserOrPass)) + }) + It("should recognize denied terminal prompt e.g. for private repo with no auth", func() { + parsed := parseErrorMessage("could not read Username for 'https://github.com': terminal prompts disabled.") + Expect(parsed.class).To(Equal(AuthPrompted)) + }) + It("should recognize non-existing repo", func() { + parsed := parseErrorMessage("Repository not found.") + Expect(parsed.class).To(Equal(RepositoryNotFound)) + }) + It("should not be able to specify exact error class for unknown message type", func() { + parsed := parseErrorMessage("Something went wrong") + Expect(parsed.class).To(Equal(Unknown)) + }) + }) + Context("If remote exists then prioritize it", func() { + It("case with repo not found", func() { + tokens := parse("remote:\nremote: ========================================================================\nremote:\nremote: The project you were looking for could not be found or you don't have permission to view it.\nremote:\nremote: ========================================================================\nremote:\nfatal: Could not read from remote repository.\n\nPlease make sure you have the correct access rights\nand the repository exists.") + errorResult := extractResultsFromTokens(tokens) + Expect(errorResult.Reason.String()).To(Equal(RepositoryNotFound.String())) + }) + }) +}) diff --git a/pkg/reconciler/buildrun/resources/sources/git.go b/pkg/reconciler/buildrun/resources/sources/git.go index 7469851679..4108b260c4 100644 --- a/pkg/reconciler/buildrun/resources/sources/git.go +++ b/pkg/reconciler/buildrun/resources/sources/git.go @@ -57,6 +57,10 @@ func AppendGitStep( fmt.Sprintf("$(results.%s-source-%s-%s.path)", prefixParamsResultsVolumes, name, commitAuthorResult), "--result-file-branch-name", fmt.Sprintf("$(results.%s-source-%s-%s.path)", prefixParamsResultsVolumes, name, branchName), + "--result-file-error-message", + fmt.Sprintf("$(results.%s-error-message.path)", prefixParamsResultsVolumes), + "--result-file-error-reason", + fmt.Sprintf("$(results.%s-error-reason.path)", prefixParamsResultsVolumes), } // Check if a revision is defined diff --git a/pkg/reconciler/buildrun/resources/sources/git_test.go b/pkg/reconciler/buildrun/resources/sources/git_test.go index 5d3e978209..3fca9669df 100644 --- a/pkg/reconciler/buildrun/resources/sources/git_test.go +++ b/pkg/reconciler/buildrun/resources/sources/git_test.go @@ -56,6 +56,10 @@ var _ = Describe("Git", func() { "$(results.shp-source-default-commit-author.path)", "--result-file-branch-name", "$(results.shp-source-default-branch-name.path)", + "--result-file-error-message", + "$(results.shp-error-message.path)", + "--result-file-error-reason", + "$(results.shp-error-reason.path)", })) }) }) @@ -106,6 +110,10 @@ var _ = Describe("Git", func() { "$(results.shp-source-default-commit-author.path)", "--result-file-branch-name", "$(results.shp-source-default-branch-name.path)", + "--result-file-error-message", + "$(results.shp-error-message.path)", + "--result-file-error-reason", + "$(results.shp-error-reason.path)", "--secret-path", "/workspace/shp-source-secret", })) diff --git a/pkg/reconciler/buildrun/resources/taskrun_test.go b/pkg/reconciler/buildrun/resources/taskrun_test.go index 45b1158c4f..5d5eac07a7 100644 --- a/pkg/reconciler/buildrun/resources/taskrun_test.go +++ b/pkg/reconciler/buildrun/resources/taskrun_test.go @@ -94,6 +94,10 @@ var _ = Describe("GenerateTaskrun", func() { "$(results.shp-source-default-commit-author.path)", "--result-file-branch-name", "$(results.shp-source-default-branch-name.path)", + "--result-file-error-message", + "$(results.shp-error-message.path)", + "--result-file-error-reason", + "$(results.shp-error-reason.path)", })) }) diff --git a/test/data/build_non_existing_repo.yaml b/test/data/build_non_existing_repo.yaml new file mode 100644 index 0000000000..52967f5b6a --- /dev/null +++ b/test/data/build_non_existing_repo.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: shipwright.io/v1alpha1 +kind: Build +metadata: + name: build-non-existing-repo +spec: + source: + url: https://github.com/shipwright-io/sample-nodejs-no-exists + strategy: + name: kaniko + kind: ClusterBuildStrategy + output: + image: image-registry.openshift-image-registry.svc:5000/build-examples/no-exists + diff --git a/test/data/buildrun_non_existing_repo.yaml b/test/data/buildrun_non_existing_repo.yaml new file mode 100644 index 0000000000..21705cb97e --- /dev/null +++ b/test/data/buildrun_non_existing_repo.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: shipwright.io/v1alpha1 +kind: BuildRun +metadata: + name: buildrun-non-existing-repo +spec: + buildRef: + name: buildrun-non-existing-repo diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 2502e7af8a..938195b536 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -10,8 +10,10 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" buildv1alpha1 "github.com/shipwright-io/build/pkg/apis/build/v1alpha1" + shpgit "github.com/shipwright-io/build/pkg/git" ) var _ = Describe("For a Kubernetes cluster with Tekton and build installed", func() { @@ -599,4 +601,26 @@ var _ = Describe("For a Kubernetes cluster with Tekton and build installed", fun }) }) }) + + Context("when a s2i build uses a non-existent git repository as source", func() { + It("fails because of prompted authentication which surfaces the to the BuildRun", func() { + testID = generateTestID("s2i-failing") + + build = createBuild( + testBuild, + testID, + "test/data/build_non_existing_repo.yaml", + ) + + buildRun, err = buildRunTestData(build.Namespace, testID, "test/data/buildrun_non_existing_repo.yaml") + Expect(err).ToNot(HaveOccurred()) + + validateBuildRunToFail(testBuild, buildRun) + buildRun, err = testBuild.LookupBuildRun(types.NamespacedName{Name: buildRun.Name, Namespace: testBuild.Namespace}) + + Expect(buildRun.Status.FailureDetails.Message).To(Equal(shpgit.AuthPrompted.ToMessage())) + Expect(buildRun.Status.FailureDetails.Reason).To(Equal(shpgit.AuthPrompted.String())) + }) + }) + })