Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,18 @@ jobs:
go-version-file: go.mod
- name: Test
run: go test -race ./...
- name: Test (nocrypto)
run: go test -race -tags gravitational_trace.nocrypto ./...
nocrypto:
name: Check nocrypto dependencies
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Check dependencies with the nocrypto build tag
shell: bash # for pipefail
run: "go list -tags gravitational_trace.nocrypto -deps ./... | sort | tee /dev/stderr | (! grep -q -e ^crypto$ -e ^crypto/ )"
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't used goda because I'm not sure of which version of go that would require, and using go list is good enough for this specific purpose.

4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ func main() {
}
```

### Build tags

The functions `WriteError`, `ReadError` and `ErrorToCode` require including the standard library package `net/http`, which transitively depends on `crypto/tls`, `crypto/x509`, and a lot of the `crypto/...` package tree, and the `ConvertSystemError` function depends on `crypto/x509` to wrap the `x509.SystemRootsError` and `x509.UnknownAuthorityError` errors into a `trace.TrustError`.


As a size optimization for binaries that don't otherwise make use of `net/http` or `crypto/x509`, builds with the `gravitational_trace.nocrypto` build tag will exclude the `WriteError`, `ReadError` and `ErrorToCode` functions, and will not match against the `x509.SystemRootsError` and `x509.UnknownAuthorityError` errors in `ConvertSystemError`, which will get rid of the requirement for `net/http` or `crypto/...`.
3 changes: 1 addition & 2 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ limitations under the License.
package trace

import (
"crypto/x509"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -347,7 +346,7 @@ func ConvertSystemError(err error) error {
return newTrace(&AccessDeniedError{
Message: message,
})
case x509.SystemRootsError, x509.UnknownAuthorityError:
case x509SystemRootsError, x509UnknownAuthorityError:
return newTrace(&TrustError{Err: innerError})
}
if _, ok := innerError.(net.Error); ok {
Expand Down
26 changes: 26 additions & 0 deletions errors_crypto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//go:build !gravitational_trace.nocrypto

// Copyright 2026 Gravitational, Inc.
//
// 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.

package trace

import "crypto/x509"

// these type aliases are used by [ConvertSystemError]; builds with the nocrypto
// tag will define them differently
type (
x509SystemRootsError = x509.SystemRootsError
x509UnknownAuthorityError = x509.UnknownAuthorityError
)
35 changes: 35 additions & 0 deletions errors_nocrypto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//go:build gravitational_trace.nocrypto
Comment thread
codingllama marked this conversation as resolved.

// Copyright 2026 Gravitational, Inc.
//
// 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.

package trace

// these unexported error types are never constructed but they are referenced by
// [ConvertSystemError]; in builds without the nocrypto tag they are aliases of
// errors in [crypto/x509]
type (
x509SystemRootsError struct{}
x509UnknownAuthorityError struct{}
)

// Error implements [error].
func (x509SystemRootsError) Error() string {
return "trace.x509SystemRootsError (this is a bug)"
}

// Error implements [error].
func (x509UnknownAuthorityError) Error() string {
return "trace.x509UnknownAuthorityError (this is a bug)"
}
16 changes: 16 additions & 0 deletions httplib.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
//go:build !gravitational_trace.nocrypto

// Copyright 2026 Gravitational, Inc.
//
// 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.

package trace

import (
Expand Down
2 changes: 2 additions & 0 deletions httplib_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !gravitational_trace.nocrypto

/*
Copyright 2020 Gravitational, Inc.

Expand Down
14 changes: 14 additions & 0 deletions internal/traverse.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
// Copyright 2026 Gravitational, Inc.
Comment thread
codingllama marked this conversation as resolved.
//
// 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.

package internal

// TraverseErr traverses the err error chain until fn returns true.
Expand Down
214 changes: 214 additions & 0 deletions trace_crypto_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
//go:build !gravitational_trace.nocrypto

// Copyright 2026 Gravitational, Inc.
//
// 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.

package trace

import (
"fmt"
"net/http"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestGetFields(t *testing.T) {
testErr := fmt.Errorf("description")
assert.Equal(t, map[string]interface{}{}, GetFields(testErr))

fields := map[string]interface{}{
"test_key": "test_value",
}
err := WithFields(Wrap(testErr), fields)
assert.Equal(t, fields, GetFields(err))

// ensure that you can get fields from a proxyError
e := roundtripError(err)
assert.Equal(t, fields, GetFields(e))
}

func roundtripError(err error) error {
w := newTestWriter()
WriteError(w, err)

outErr := ReadError(w.StatusCode, w.Body)
return outErr
}

func TestGenericErrors(t *testing.T) {
testCases := []struct {
Err Error
Predicate func(error) bool
StatusCode int
comment string
}{
{
Err: NotFound("not found"),
Predicate: IsNotFound,
StatusCode: http.StatusNotFound,
comment: "not found error",
},
{
Err: AlreadyExists("already exists"),
Predicate: IsAlreadyExists,
StatusCode: http.StatusConflict,
comment: "already exists error",
},
{
Err: BadParameter("is bad"),
Predicate: IsBadParameter,
StatusCode: http.StatusBadRequest,
comment: "bad parameter error",
},
{
Err: CompareFailed("is bad"),
Predicate: IsCompareFailed,
StatusCode: http.StatusPreconditionFailed,
comment: "comparison failed error",
},
{
Err: AccessDenied("denied"),
Predicate: IsAccessDenied,
StatusCode: http.StatusForbidden,
comment: "access denied error",
},
{
Err: ConnectionProblem(nil, "prob"),
Predicate: IsConnectionProblem,
StatusCode: http.StatusRequestTimeout,
comment: "connection error",
},
{
Err: LimitExceeded("limit exceeded"),
Predicate: IsLimitExceeded,
StatusCode: http.StatusTooManyRequests,
comment: "limit exceeded error",
},
{
Err: NotImplemented("not implemented"),
Predicate: IsNotImplemented,
StatusCode: http.StatusNotImplemented,
comment: "not implemented error",
},
}

for _, testCase := range testCases {
SetDebug(true)
err := testCase.Err

var traceErr *TraceErr
var ok bool
if traceErr, ok = err.(*TraceErr); !ok {
t.Fatalf("Expected error to be of type *TraceErr: %#v", err)
}

assert.NotEmpty(t, traceErr.Traces, testCase.comment)
assert.Regexp(t, ".*.trace_crypto_test\\.go.*", line(DebugReport(err)), testCase.comment)
assert.NotRegexp(t, ".*.errors\\.go.*", line(DebugReport(err)), testCase.comment)
assert.NotRegexp(t, ".*.trace\\.go.*", line(DebugReport(err)), testCase.comment)
assert.True(t, testCase.Predicate(err), testCase.comment)

w := newTestWriter()
WriteError(w, err)

outErr := ReadError(w.StatusCode, w.Body)
if _, ok := outErr.(proxyError); !ok {
t.Fatalf("Expected error to be of type proxyError: %#v", outErr)
}
assert.True(t, testCase.Predicate(outErr), testCase.comment)

SetDebug(false)
w = newTestWriter()
WriteError(w, err)
outErr = ReadError(w.StatusCode, w.Body)
assert.True(t, testCase.Predicate(outErr), testCase.comment)
}
}

// Make sure we write some output produced by standard errors
Comment thread
codingllama marked this conversation as resolved.
func TestWriteExternalErrors(t *testing.T) {
err := Wrap(fmt.Errorf("snap!"))

SetDebug(true)
w := newTestWriter()
WriteError(w, err)
extErr := ReadError(w.StatusCode, w.Body)
assert.Equal(t, http.StatusInternalServerError, w.StatusCode)
assert.Regexp(t, ".*.snap.*", strings.Replace(string(w.Body), "\n", "", -1))
require.NotNil(t, extErr)
assert.EqualError(t, err, extErr.Error())

SetDebug(false)
w = newTestWriter()
WriteError(w, err)
extErr = ReadError(w.StatusCode, w.Body)
assert.Equal(t, http.StatusInternalServerError, w.StatusCode)
assert.Regexp(t, ".*.snap.*", strings.Replace(string(w.Body), "\n", "", -1))
require.NotNil(t, extErr)
assert.EqualError(t, err, extErr.Error())
Comment thread
codingllama marked this conversation as resolved.
}

func TestAggregateConvertsToCommonErrors(t *testing.T) {
testCases := []struct {
Err error
Predicate func(error) bool
RoundtripPredicate func(error) bool
StatusCode int
comment string
}{
{
comment: "Aggregate unwraps to first aggregated error",
Err: NewAggregate(
BadParameter("invalid value of foo"),
LimitExceeded("limit exceeded"),
),
Predicate: IsAggregate,
RoundtripPredicate: IsBadParameter,
StatusCode: http.StatusBadRequest,
},
{
comment: "Nested aggregate unwraps recursively",
Err: NewAggregate(
NewAggregate(
BadParameter("invalid value of foo"),
LimitExceeded("limit exceeded"),
),
),
Predicate: IsAggregate,
RoundtripPredicate: IsBadParameter,
StatusCode: http.StatusBadRequest,
},
}
for _, testCase := range testCases {
SetDebug(true)
err := testCase.Err

assert.Regexp(t, ".*.trace_crypto_test.go.*", line(DebugReport(err)), testCase.comment)
assert.True(t, testCase.Predicate(err), testCase.comment)

w := newTestWriter()
WriteError(w, err)
outErr := ReadError(w.StatusCode, w.Body)
assert.True(t, testCase.RoundtripPredicate(outErr), testCase.comment)

SetDebug(false)
w = newTestWriter()
WriteError(w, err)
outErr = ReadError(w.StatusCode, w.Body)
assert.True(t, testCase.RoundtripPredicate(outErr), testCase.comment)
}
}
Loading
Loading