Skip to content

Commit

Permalink
Support passing custom headers to the provider (#78)
Browse files Browse the repository at this point in the history
Fixes #73
  • Loading branch information
tdabasinskas committed Jan 5, 2024
2 parents e50719f + 17d1789 commit d270781
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 9 deletions.
23 changes: 18 additions & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,23 @@ jobs:
git diff --compact-summary --exit-code || \
(echo; echo "Unexpected difference in directories after code generation. Run 'go generate ./...' command and commit."; exit 1)
test-internal:
name: Internal Tests
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
with:
go-version-file: 'go.mod'
cache: true
- run: go mod download
- run: go test -v ./internal/...

# Run acceptance tests in a matrix with Terraform CLI versions
test:
test-terraform:
name: Acceptance Tests
needs: build
needs: test-internal
runs-on: ubuntu-latest
env:
TF_ACC: "1"
Expand All @@ -69,9 +82,9 @@ jobs:
matrix:
# list whatever Terraform versions here you would like to support
terraform:
- '1.0.*'
- '1.1.*'
- '1.2.*'
- '1.4.*'
- '1.5.*'
- '1.6.*'
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
Expand Down
36 changes: 32 additions & 4 deletions backstage/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@ package backstage
import (
"context"
"fmt"
"os"
"regexp"

"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/path"
Expand All @@ -16,6 +13,10 @@ import (
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/tdabasinskas/go-backstage/v2/backstage"
"github.com/tdabasinskas/terraform-provider-backstage/internal/transport"
"net/http"
"os"
"regexp"
)

var _ provider.Provider = &backstageProvider{}
Expand All @@ -29,16 +30,20 @@ type backstageProvider struct {
type backstageProviderModel struct {
BaseURL types.String `tfsdk:"base_url"`
DefaultNamespace types.String `tfsdk:"default_namespace"`
Headers types.Map `tfsdk:"headers"`
}

const (
patternURL = "https?://.+"
envBaseURL = "BACKSTAGE_BASE_URL"
envDefaultNamespace = "BACKSTAGE_DEFAULT_NAMESPACE"
envHeaders = "BACKSTAGE_HEADERS"
descriptionProviderBaseURL = "Base URL of the Backstage instance, e.g. https://demo.backstage.io. May also be provided via `" + envBaseURL +
"` environment variable."
descriptionProviderDefaultNamespace = "Name of default namespace for entities (`default`, if not set). May also be provided via `" + envDefaultNamespace +
"` environment variable."
descriptionProviderHeaders = "Headers to be sent with each request to the Backstage API. Useful for authentication. May also be provided via `" + envHeaders +
"` environment variable."
)

// Metadata returns the provider type name.
Expand All @@ -64,6 +69,7 @@ func (p *backstageProvider) Schema(_ context.Context, _ provider.SchemaRequest,
stringvalidator.LengthBetween(1, 63),
stringvalidator.RegexMatches(regexp.MustCompile(patternEntityName), "must follow Backstage format restrictions"),
}},
"headers": schema.MapAttribute{Optional: true, ElementType: types.StringType, MarkdownDescription: descriptionProviderHeaders},
},
}
}
Expand Down Expand Up @@ -119,16 +125,38 @@ func (p *backstageProvider) Configure(ctx context.Context, req provider.Configur
"configuration or use the %s environment variable. If either is already set, ensure the value is not empty and valid.", envDefaultNamespace))
}

headers := make(map[string]string)
if headersEnv := os.Getenv(envHeaders); headersEnv != "" {
for _, kv := range regexp.MustCompile(`(.*?)=([^=]*)(?:,|$)`).FindAllStringSubmatch(headersEnv, -1) {
headers[kv[1]] = kv[2]
}
} else {
if !config.Headers.IsNull() {
for k, v := range config.Headers.Elements() {
headers[k] = v.String()
}
}
}

if resp.Diagnostics.HasError() {
return
}

ctx = tflog.SetField(ctx, "backstage_base_url", baseURL)
ctx = tflog.SetField(ctx, "backstage_default_namespace", defaultNamespace)
ctx = tflog.SetField(ctx, "backstage_headers", headers)

tflog.Debug(ctx, "Creating Backstage API client")

client, err := backstage.NewClient(baseURL, defaultNamespace, nil)
var baseClient = &http.Client{}
if len(headers) > 0 {
baseClient = &http.Client{
Transport: &transport.HeadersTransport{
Headers: headers,
},
}
}
client, err := backstage.NewClient(baseURL, defaultNamespace, baseClient)
if err != nil {
resp.Diagnostics.AddError("Unable to create Backstage API client",
fmt.Sprintf("An unexpected error occurred when creating the Backstage API client: %s", err.Error()),
Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@ provider "backstage" {

- `base_url` (String) Base URL of the Backstage instance, e.g. https://demo.backstage.io. May also be provided via `BACKSTAGE_BASE_URL` environment variable.
- `default_namespace` (String) Name of default namespace for entities (`default`, if not set). May also be provided via `BACKSTAGE_DEFAULT_NAMESPACE` environment variable.
- `headers` (Map of String) Headers to be sent with each request to the Backstage API. Useful for authentication. May also be provided via `BACKSTAGE_HEADERS` environment variable.
6 changes: 6 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ module github.com/tdabasinskas/terraform-provider-backstage
go 1.21

require (
github.com/h2non/gock v1.2.0
github.com/hashicorp/terraform-plugin-docs v0.16.0
github.com/hashicorp/terraform-plugin-framework v1.4.2
github.com/hashicorp/terraform-plugin-framework-validators v0.12.0
github.com/hashicorp/terraform-plugin-go v0.20.0
github.com/hashicorp/terraform-plugin-log v0.9.0
github.com/hashicorp/terraform-plugin-sdk/v2 v2.31.0
github.com/stretchr/testify v1.8.4
github.com/tdabasinskas/go-backstage/v2 v2.1.0
)

Expand All @@ -22,10 +24,12 @@ require (
github.com/armon/go-radix v1.0.0 // indirect
github.com/bgentry/speakeasy v0.1.0 // indirect
github.com/cloudflare/circl v1.3.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-checkpoint v0.5.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
Expand Down Expand Up @@ -54,6 +58,7 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/oklog/run v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/posener/complete v1.2.3 // indirect
github.com/russross/blackfriday v1.6.0 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
Expand All @@ -72,4 +77,5 @@ require (
google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 // indirect
google.golang.org/grpc v1.60.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
2 changes: 2 additions & 0 deletions go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 50 additions & 0 deletions internal/transport/headers_transport.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package transport

import "net/http"

// HeadersTransport is a http.RoundTripper that supports adding custom HTTP headers to requests.
type HeadersTransport struct {
// Headers is a set of headers to add to each request.
Headers map[string]string

// BaseTransport is the underlying HTTP transport to use when making requests. It will default to http.DefaultTransport if nil.
BaseTransport http.RoundTripper
}

// RoundTrip implements the RoundTripper interface.
func (t *HeadersTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req = cloneRequest(req)

for k, v := range t.Headers {
req.Header.Add(k, v)
}

return t.transport().RoundTrip(req)
}

// Client returns an *http.Client that makes requests that include additional headers.
func (t *HeadersTransport) Client() *http.Client {
return &http.Client{Transport: t}
}

// transport returns the underlying HTTP transport. If none is set, http.DefaultTransport is used.
func (t *HeadersTransport) transport() http.RoundTripper {
if t.BaseTransport != nil {
return t.BaseTransport
}

return http.DefaultTransport
}

// cloneRequest returns a clone of the provided *http.Request. The clone is a shallow copy of the struct and its headers map.
func cloneRequest(r *http.Request) *http.Request {
r2 := new(http.Request)
*r2 = *r
r2.Header = make(http.Header, len(r.Header))

for k, s := range r.Header {
r2.Header[k] = append([]string(nil), s...)
}

return r2
}
35 changes: 35 additions & 0 deletions internal/transport/headers_transport_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package transport

import (
"context"
"github.com/h2non/gock"
"github.com/stretchr/testify/assert"
"github.com/tdabasinskas/go-backstage/v2/backstage"
"net/http"
"testing"
)

func TestHeadersTransport_HeadersAdded(t *testing.T) {
const baseURL = "http://localhost:7007"

headers := map[string]string{
"test-header-1": "test-value-1",
"test-header-2": "test-value-2",
}

defer gock.Off()
gock.New(baseURL).
MatchHeaders(headers).
Reply(http.StatusOK)

client, err := backstage.NewClient(baseURL, "default", &http.Client{
Transport: &HeadersTransport{
Headers: headers,
},
})

assert.NoErrorf(t, err, "NewClient should not return an error")
_, _, err = client.Catalog.Entities.List(context.Background(), &backstage.ListEntityOptions{})

assert.NoErrorf(t, err, "ListEntities should not return an error")
}

0 comments on commit d270781

Please sign in to comment.