Skip to content

Commit

Permalink
Added git-err parsing and surface failures
Browse files Browse the repository at this point in the history
Adds Parser that processes git stdout

Git cmd line tool now uses error results to surface detailed reason and message
  • Loading branch information
Baran Dalgic committed Jan 11, 2022
1 parent a62a313 commit df19a02
Show file tree
Hide file tree
Showing 11 changed files with 642 additions and 4 deletions.
54 changes: 50 additions & 4 deletions cmd/git/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"regexp"
"strings"

shpgit "github.com/shipwright-io/build/pkg/git"
"github.com/spf13/pflag"
)

Expand All @@ -36,6 +37,7 @@ type ExitError struct {
Code int
Message string
Cause error
Reason shpgit.ErrorClass
}

func (e ExitError) Error() string {
Expand All @@ -54,6 +56,8 @@ type settings struct {
secretPath string
skipValidation bool
gitURLRewrite bool
resultFileErrorMessage string
resultFileErrorReason string
}

var flagValues settings
Expand All @@ -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).
Expand All @@ -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)
}
Expand Down Expand Up @@ -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`
Expand All @@ -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
}
134 changes: 134 additions & 0 deletions cmd/git/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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)
})
})
})
18 changes: 18 additions & 0 deletions docs/buildrun.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
Loading

0 comments on commit df19a02

Please sign in to comment.