Skip to content

Commit

Permalink
feat: key management support
Browse files Browse the repository at this point in the history
  • Loading branch information
jharley committed Jul 15, 2024
1 parent 58def89 commit 36ca9b0
Show file tree
Hide file tree
Showing 54 changed files with 1,724 additions and 167 deletions.
10 changes: 10 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ jobs:
timeout-minutes: 10
env:
HONEYCOMB_API_KEY: ${{ secrets.HONEYCOMB_API_KEY }}
HONEYCOMB_KEY_ID: ${{ secrets.HONEYCOMB_KEY_ID }}
HONEYCOMB_KEY_SECRET: ${{ secrets.HONEYCOMB_KEY_SECRET }}
HONEYCOMB_DATASET: testacc
run: go test -v -coverprofile=client-coverage.txt -covermode=atomic ./client/...

Expand All @@ -73,7 +75,10 @@ jobs:
timeout-minutes: 10
env:
HONEYCOMB_API_KEY: ${{ secrets.HONEYCOMB_API_KEY }}
HONEYCOMB_KEY_ID: ${{ secrets.HONEYCOMB_KEY_ID }}
HONEYCOMB_KEY_SECRET: ${{ secrets.HONEYCOMB_KEY_SECRET }}
HONEYCOMB_DATASET: testacc
HONEYCOMB_ENVIRONMENT_ID: hcaen_01j1d7t02zf7wgw7q89z3t60vf # TODO: remove and do a lookup or create in tests
TF_ACC: 1
TF_ACC_TERRAFORM_VERSION: ${{ env.TERRAFORM_VERSION }}
run: go test -v -coverprofile=tf-coverage.txt -covermode=atomic ./internal/... ./honeycombio/...
Expand Down Expand Up @@ -112,6 +117,8 @@ jobs:
env:
HONEYCOMB_API_ENDPOINT: https://api.eu1.honeycomb.io
HONEYCOMB_API_KEY: ${{ secrets.HONEYCOMB_API_KEY_EU }}
HONEYCOMB_KEY_ID: ${{ secrets.HONEYCOMB_KEY_ID_EU }}
HONEYCOMB_KEY_SECRET: ${{ secrets.HONEYCOMB_KEY_SECRET_EU }}
HONEYCOMB_DATASET: testacc
run: go test -v -coverprofile=client-coverage.txt -covermode=atomic ./client/...

Expand All @@ -125,7 +132,10 @@ jobs:
env:
HONEYCOMB_API_ENDPOINT: https://api.eu1.honeycomb.io
HONEYCOMB_API_KEY: ${{ secrets.HONEYCOMB_API_KEY_EU }}
HONEYCOMB_KEY_ID: ${{ secrets.HONEYCOMB_KEY_ID_EU }}
HONEYCOMB_KEY_SECRET: ${{ secrets.HONEYCOMB_KEY_SECRET_EU }}
HONEYCOMB_DATASET: testacc
HONEYCOMB_ENVIRONMENT_ID: hcben_01hvp28qbgzaeebbz29qvbb7gt # TODO: remove and do a lookup or create in tests
TF_ACC: 1
TF_ACC_TERRAFORM_VERSION: ${{ env.TERRAFORM_VERSION }}
run: go test -v -coverprofile=tf-coverage.txt -covermode=atomic ./internal/... ./honeycombio/...
Expand Down
3 changes: 2 additions & 1 deletion client/board_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/stretchr/testify/require"

"github.com/honeycombio/terraform-provider-honeycombio/client"
"github.com/honeycombio/terraform-provider-honeycombio/client/errors"
"github.com/honeycombio/terraform-provider-honeycombio/internal/helper/test"
)

Expand Down Expand Up @@ -139,7 +140,7 @@ func TestBoards(t *testing.T) {
t.Run("Fail to get deleted Board", func(t *testing.T) {
_, err := c.Boards.Get(ctx, b.ID)

var de client.DetailedError
var de errors.DetailedError
require.Error(t, err)
require.ErrorAs(t, err, &de)
assert.True(t, de.IsNotFound())
Expand Down
3 changes: 2 additions & 1 deletion client/burn_alert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/require"

"github.com/honeycombio/terraform-provider-honeycombio/client"
"github.com/honeycombio/terraform-provider-honeycombio/client/errors"
"github.com/honeycombio/terraform-provider-honeycombio/internal/helper/test"
)

Expand Down Expand Up @@ -219,7 +220,7 @@ func TestBurnAlerts(t *testing.T) {
t.Run(fmt.Sprintf("Fail to GET a deleted burn alert: %s", testName), func(t *testing.T) {
_, err := c.BurnAlerts.Get(ctx, dataset, burnAlert.ID)

var de client.DetailedError
var de errors.DetailedError
require.Error(t, err)
require.ErrorAs(t, err, &de)
assert.True(t, de.IsNotFound())
Expand Down
4 changes: 3 additions & 1 deletion client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import (

cleanhttp "github.com/hashicorp/go-cleanhttp"
retryablehttp "github.com/hashicorp/go-retryablehttp"

hnyerr "github.com/honeycombio/terraform-provider-honeycombio/client/errors"
)

const (
Expand Down Expand Up @@ -221,7 +223,7 @@ func (c *Client) Do(ctx context.Context, method, path string, requestBody, respo
defer resp.Body.Close()

if !(resp.StatusCode >= 200 && resp.StatusCode <= 299) {
return errorFromResponse(resp)
return hnyerr.FromResponse(resp)
}
if responseBody != nil {
err = json.NewDecoder(resp.Body).Decode(responseBody)
Expand Down
47 changes: 47 additions & 0 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package client_test

import (
"context"
"fmt"
"net/http"
"os"
"testing"

Expand All @@ -10,6 +12,7 @@ import (
"github.com/stretchr/testify/require"

"github.com/honeycombio/terraform-provider-honeycombio/client"
"github.com/honeycombio/terraform-provider-honeycombio/client/errors"
)

const testUserAgent = "go-honeycombio/test"
Expand Down Expand Up @@ -90,3 +93,47 @@ func TestClient_EndpointURL(t *testing.T) {

assert.Equal(t, endpointUrl, c.EndpointURL().String())
}

func TestClient_Errors(t *testing.T) {
t.Parallel()

var de errors.DetailedError
ctx := context.Background()
c := newTestClient(t)

t.Run("Post with no body should fail with 400 unparseable", func(t *testing.T) {
err := c.Do(ctx, "POST", "/1/boards/", nil, nil)
require.Error(t, err)
require.ErrorAs(t, err, &de)
assert.Equal(t, http.StatusBadRequest, de.Status)
assert.Equal(t, fmt.Sprintf("%s/problems/unparseable", c.EndpointURL()), de.Type)
assert.Equal(t, "The request body could not be parsed.", de.Title)
assert.Equal(t, "could not parse request body", de.Message)
})

t.Run("Get into non-existent dataset should fail with 404 'Dataset not found'", func(t *testing.T) {
_, err := c.Markers.Get(ctx, "non-existent-dataset", "abcd1234")
require.Error(t, err)
require.ErrorAs(t, err, &de)
assert.Equal(t, http.StatusNotFound, de.Status)
assert.Equal(t, fmt.Sprintf("%s/problems/not-found", c.EndpointURL()), de.Type)
assert.Equal(t, "The requested resource cannot be found.", de.Title)
assert.Equal(t, "Dataset not found", de.Message)
})

t.Run("Creating a dataset without a name should return a validation error", func(t *testing.T) {
createDatasetRequest := &client.Dataset{}
_, err := c.Datasets.Create(ctx, createDatasetRequest)
require.Error(t, err)
require.ErrorAs(t, err, &de)
assert.Equal(t, http.StatusUnprocessableEntity, de.Status)
assert.Equal(t, fmt.Sprintf("%s/problems/validation-failed", c.EndpointURL()), de.Type)
assert.Equal(t, "The provided input is invalid.", de.Title)
assert.Equal(t, "The provided input is invalid.", de.Message)
assert.Len(t, de.Details, 1)
assert.Equal(t, "missing", de.Details[0].Code)
assert.Equal(t, "name", de.Details[0].Field)
assert.Equal(t, "cannot be blank", de.Details[0].Description)
assert.Equal(t, "missing name - cannot be blank", de.Error())
})
}
3 changes: 2 additions & 1 deletion client/column_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/stretchr/testify/require"

"github.com/honeycombio/terraform-provider-honeycombio/client"
"github.com/honeycombio/terraform-provider-honeycombio/client/errors"
"github.com/honeycombio/terraform-provider-honeycombio/internal/helper/test"
)

Expand Down Expand Up @@ -95,7 +96,7 @@ func TestColumns(t *testing.T) {
t.Run("Fail to get deleted Column", func(t *testing.T) {
_, err := c.Columns.Get(ctx, dataset, column.ID)

var de client.DetailedError
var de errors.DetailedError
require.Error(t, err)
require.ErrorAs(t, err, &de)
assert.True(t, de.IsNotFound())
Expand Down
3 changes: 2 additions & 1 deletion client/dataset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/stretchr/testify/require"

"github.com/honeycombio/terraform-provider-honeycombio/client"
"github.com/honeycombio/terraform-provider-honeycombio/client/errors"
)

func TestDatasets(t *testing.T) {
Expand Down Expand Up @@ -55,7 +56,7 @@ func TestDatasets(t *testing.T) {
t.Run("Fail to Get bogus Dataset", func(t *testing.T) {
_, err := c.Datasets.Get(ctx, "does-not-exist")

var de client.DetailedError
var de errors.DetailedError
require.Error(t, err)
require.ErrorAs(t, err, &de)
assert.True(t, de.IsNotFound())
Expand Down
5 changes: 3 additions & 2 deletions client/derived_column_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/require"

"github.com/honeycombio/terraform-provider-honeycombio/client"
"github.com/honeycombio/terraform-provider-honeycombio/client/errors"
"github.com/honeycombio/terraform-provider-honeycombio/internal/helper/test"
)

Expand Down Expand Up @@ -45,7 +46,7 @@ func TestDerivedColumns(t *testing.T) {
}
_, err = c.DerivedColumns.Create(ctx, dataset, data)

var de client.DetailedError
var de errors.DetailedError
require.Error(t, err)
require.ErrorAs(t, err, &de)
assert.Equal(t, http.StatusConflict, de.Status)
Expand Down Expand Up @@ -97,7 +98,7 @@ func TestDerivedColumns(t *testing.T) {
t.Run("Fail to Get Deleted DC", func(t *testing.T) {
_, err := c.DerivedColumns.Get(ctx, dataset, derivedColumn.ID)

var de client.DetailedError
var de errors.DetailedError
require.Error(t, err)
require.ErrorAs(t, err, &de)
assert.True(t, de.IsNotFound())
Expand Down
90 changes: 61 additions & 29 deletions client/errors.go → client/errors/errors.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package client
package errors

import (
"encoding/json"
"fmt"
"io"
"errors"
"net/http"

"github.com/hashicorp/jsonapi"
)

// DetailedError is an RFC7807 'Problem Detail' formatted error message.
type DetailedError struct {
// The HTTP status code of the error.
Status int `json:"status,omitempty"`
Expand Down Expand Up @@ -52,6 +52,14 @@ func (td ErrorTypeDetail) String() string {
return response
}

// IsNotFound returns true if the error is an HTTP 404
func (e *DetailedError) IsNotFound() bool {
if e == nil {
return false
}
return e.Status == http.StatusNotFound
}

// Error returns a pretty-printed representation of the error
func (e DetailedError) Error() string {
if len(e.Details) > 0 {
Expand All @@ -60,7 +68,8 @@ func (e DetailedError) Error() string {
for index, details := range e.Details {
response += details.String()

// If we haven't reached the end of the list of error details, add a newline separator between each error
// If we haven't reached the end of the list of error details,
// add a newline separator between each error
if index < len(e.Details)-1 {
response += "\n"
}
Expand All @@ -72,34 +81,57 @@ func (e DetailedError) Error() string {
return e.Message
}

// IsNotFound returns true if the error is an HTTP 404
func (e *DetailedError) IsNotFound() bool {
if e == nil {
return false
func FromResponse(r *http.Response) error {
if r == nil {
return errors.New("invalid response")
}
return e.Status == http.StatusNotFound
}

func errorFromResponse(resp *http.Response) error {
e, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("unable to read response body: %w", err)
}
switch r.Header.Get("Content-Type") {
case jsonapi.MediaType:
var detailedError DetailedError

var detailedErr DetailedError
err = json.Unmarshal(e, &detailedErr)
if err != nil {
// we failed to parse the body as a DetailedError, so build one from what we know
return DetailedError{
Status: resp.StatusCode,
Message: resp.Status,
errPayload := new(jsonapi.ErrorsPayload)
err := json.NewDecoder(r.Body).Decode(errPayload)
if err != nil || len(errPayload.Errors) == 0 {
return DetailedError{
Status: r.StatusCode,
Message: r.Status,
}
}
}

// quick sanity check to make sure we got a StatusCode
if detailedErr.Status == 0 {
detailedErr.Status = resp.StatusCode
}
detailedError = DetailedError{
Status: r.StatusCode,
Title: errPayload.Errors[0].Title,
}
if len(errPayload.Errors) == 1 {
// If there's only one error we don't need to build up details
detailedError.Message = errPayload.Errors[0].Detail
detailedError.Type = errPayload.Errors[0].Code
} else {
details := make([]ErrorTypeDetail, len(errPayload.Errors))
for i, e := range errPayload.Errors {
details[i] = ErrorTypeDetail{
Code: e.Code,
Description: e.Detail,
// TODO: field when we have it via pointer
}
}
}
return detailedError
default:
var detailedError DetailedError
if err := json.NewDecoder(r.Body).Decode(&detailedError); err != nil {
// If we can't decode the error, return a generic error
return DetailedError{
Status: r.StatusCode,
Message: r.Status,
}
}

return detailedErr
// sanity check: ensure we have the status code set
if detailedError.Status == 0 {
detailedError.Status = r.StatusCode
}
return detailedError
}
}
Loading

0 comments on commit 36ca9b0

Please sign in to comment.