From 81304bfc19850e8090ad62feff7f519ec3ee15a4 Mon Sep 17 00:00:00 2001 From: Tomas Dabasinskas Date: Fri, 5 Jan 2024 12:52:32 +0200 Subject: [PATCH 1/9] Add custom transport for injecting headers Took 36 minutes --- internal/transport/headers_transport.go | 50 ++++++++++++++++++++ internal/transport/headers_transport_test.go | 1 + 2 files changed, 51 insertions(+) create mode 100644 internal/transport/headers_transport.go create mode 100644 internal/transport/headers_transport_test.go diff --git a/internal/transport/headers_transport.go b/internal/transport/headers_transport.go new file mode 100644 index 0000000..86e2c5a --- /dev/null +++ b/internal/transport/headers_transport.go @@ -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 +} diff --git a/internal/transport/headers_transport_test.go b/internal/transport/headers_transport_test.go new file mode 100644 index 0000000..d11d0be --- /dev/null +++ b/internal/transport/headers_transport_test.go @@ -0,0 +1 @@ +package transport From 8844d4e406b2da6e08c53c9c778fcfc513346350 Mon Sep 17 00:00:00 2001 From: Tomas Dabasinskas Date: Fri, 5 Jan 2024 12:52:49 +0200 Subject: [PATCH 2/9] Add tests for the custom transport --- go.mod | 6 ++++ go.sum | 2 ++ internal/transport/headers_transport_test.go | 34 ++++++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/go.mod b/go.mod index f76da4d..2fb5d50 100644 --- a/go.mod +++ b/go.mod @@ -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 ) @@ -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 @@ -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 @@ -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 ) diff --git a/go.sum b/go.sum index d1f05fc..97c7134 100644 --- a/go.sum +++ b/go.sum @@ -160,6 +160,8 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= +github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= diff --git a/internal/transport/headers_transport_test.go b/internal/transport/headers_transport_test.go index d11d0be..c229c96 100644 --- a/internal/transport/headers_transport_test.go +++ b/internal/transport/headers_transport_test.go @@ -1 +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") +} From 3831dd27c6da113ee85d475606b36b6f65632818 Mon Sep 17 00:00:00 2001 From: Tomas Dabasinskas Date: Fri, 5 Jan 2024 12:55:17 +0200 Subject: [PATCH 3/9] Run internal tests during CI/CD Took 3 minutes --- .github/workflows/test.yml | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 01e46c4..c50e028 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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" From 042a3964cd7adb4176127d7dc7eaad07e3364078 Mon Sep 17 00:00:00 2001 From: Tomas Dabasinskas Date: Fri, 5 Jan 2024 12:58:58 +0200 Subject: [PATCH 4/9] Fix pipeline Took 4 minutes --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c50e028..baed613 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -62,7 +62,7 @@ jobs: go-version-file: 'go.mod' cache: true - run: go mod download - - run: go test -v ./internal + - run: go test -v ./internal/... # Run acceptance tests in a matrix with Terraform CLI versions test-terraform: From 925f3ead74d8a0a4b65ee3b538ded120f6dbd330 Mon Sep 17 00:00:00 2001 From: Tomas Dabasinskas Date: Fri, 5 Jan 2024 13:06:00 +0200 Subject: [PATCH 5/9] Update tests to run on newer TF versions Took 3 minutes # Commit time for manual adjustment: # Took 2 minutes --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index baed613..d726c5d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -82,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 From 223706ae15f99bfcd08d595a008911ba69e1eb3c Mon Sep 17 00:00:00 2001 From: Tomas Dabasinskas Date: Fri, 5 Jan 2024 13:47:38 +0200 Subject: [PATCH 6/9] Support passing custom headers Took 32 minutes --- backstage/provider.go | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/backstage/provider.go b/backstage/provider.go index 8cd8635..f004627 100644 --- a/backstage/provider.go +++ b/backstage/provider.go @@ -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" @@ -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{} @@ -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 `" + envDefaultNamespace + + "` environment variable." ) // Metadata returns the provider type name. @@ -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}, }, } } @@ -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 !config.Headers.IsNull() { + for k, v := range config.Headers.Elements() { + headers[k] = v.String() + } + } else { + if headersEnv := os.Getenv(envHeaders); headersEnv != "" { + for _, kv := range regexp.MustCompile(`(.*?)=([^=]*)(?:,|$)`).FindAllStringSubmatch(headersEnv, -1) { + headers[kv[1]] = kv[2] + } + } + } + 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()), From 71c6b25160b7a6beabcac5f721901f1dee324c77 Mon Sep 17 00:00:00 2001 From: Tomas Dabasinskas Date: Fri, 5 Jan 2024 13:49:01 +0200 Subject: [PATCH 7/9] Update docs Took 2 minutes --- docs/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/index.md b/docs/index.md index 308058f..ce15ead 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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_DEFAULT_NAMESPACE` environment variable. From c57fb0fad754bb7c522e9aa4bcaa3363a8ea061b Mon Sep 17 00:00:00 2001 From: Tomas Dabasinskas Date: Fri, 5 Jan 2024 13:51:26 +0200 Subject: [PATCH 8/9] Prefer envar over parameter Took 2 minutes --- backstage/provider.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/backstage/provider.go b/backstage/provider.go index f004627..b8d3705 100644 --- a/backstage/provider.go +++ b/backstage/provider.go @@ -126,14 +126,14 @@ func (p *backstageProvider) Configure(ctx context.Context, req provider.Configur } headers := make(map[string]string) - if !config.Headers.IsNull() { - for k, v := range config.Headers.Elements() { - headers[k] = v.String() + if headersEnv := os.Getenv(envHeaders); headersEnv != "" { + for _, kv := range regexp.MustCompile(`(.*?)=([^=]*)(?:,|$)`).FindAllStringSubmatch(headersEnv, -1) { + headers[kv[1]] = kv[2] } } else { - if headersEnv := os.Getenv(envHeaders); headersEnv != "" { - for _, kv := range regexp.MustCompile(`(.*?)=([^=]*)(?:,|$)`).FindAllStringSubmatch(headersEnv, -1) { - headers[kv[1]] = kv[2] + if !config.Headers.IsNull() { + for k, v := range config.Headers.Elements() { + headers[k] = v.String() } } } From 17d1789fa15a7721644ce18beb6dff9a625cee6e Mon Sep 17 00:00:00 2001 From: Tomas Dabasinskas Date: Fri, 5 Jan 2024 13:54:06 +0200 Subject: [PATCH 9/9] Update parameter description Took 3 minutes --- backstage/provider.go | 2 +- docs/index.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backstage/provider.go b/backstage/provider.go index b8d3705..bcda4fd 100644 --- a/backstage/provider.go +++ b/backstage/provider.go @@ -42,7 +42,7 @@ const ( "` 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 `" + envDefaultNamespace + + descriptionProviderHeaders = "Headers to be sent with each request to the Backstage API. Useful for authentication. May also be provided via `" + envHeaders + "` environment variable." ) diff --git a/docs/index.md b/docs/index.md index ce15ead..f0e16a5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -38,4 +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_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.