diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a9dfe2..4fda38d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v0.6.3] - Unreleased + +### Added + +- Added the "User-Agent" header injected to every request to the Neon API for tracking purposes as agreed with + James Broadhead from Neon. + +### Changed + +- Updated dependencies: + - Neon Go SDK: [v0.7.0](https://github.com/kislerdm/neon-sdk-go/compare/v0.6.1...v0.7.0) + ## [v0.6.2] - 2024-10-04 ### Added diff --git a/go.mod b/go.mod index c9f22c0..8d34842 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/hashicorp/terraform-plugin-docs v0.19.4 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.33.0 - github.com/kislerdm/neon-sdk-go v0.6.1 + github.com/kislerdm/neon-sdk-go v0.7.0 github.com/stretchr/testify v1.8.2 ) diff --git a/go.sum b/go.sum index 5dc3635..d434262 100644 --- a/go.sum +++ b/go.sum @@ -124,6 +124,8 @@ github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4 github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kislerdm/neon-sdk-go v0.6.1 h1:ILnqrZzRsjbAnxXOx3wb9HKUGR21pv14Y7U3yibUN00= github.com/kislerdm/neon-sdk-go v0.6.1/go.mod h1:WSwEZ7oeR5KfQoCuDh/04LZxnSKDcvfsZyfG/QicDb8= +github.com/kislerdm/neon-sdk-go v0.7.0 h1:Xboh1mZO0jM8ss6u6iuVBkgBk9rXeq0PyoNisxi/fUA= +github.com/kislerdm/neon-sdk-go v0.7.0/go.mod h1:WSwEZ7oeR5KfQoCuDh/04LZxnSKDcvfsZyfG/QicDb8= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= diff --git a/internal/provider/provider.go b/internal/provider/provider.go index a11d612..2f659f9 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -2,6 +2,7 @@ package provider import ( "context" + "fmt" "math/rand" "net/http" "os" @@ -68,6 +69,10 @@ func newDev() *schema.Provider { } func New(version string) *schema.Provider { + return newWithClient(version, nil) +} + +func newWithClient(version string, customHTTPClient neon.HTTPClient) *schema.Provider { return &schema.Provider{ Schema: map[string]*schema.Schema{ "api_key": { @@ -92,23 +97,48 @@ func New(version string) *schema.Provider { "neon_branch_roles": dataSourceBranchRoles(), "neon_branch_role_password": dataSourceBranchRolePassword(), }, - ConfigureContextFunc: configure(version), + ConfigureContextFunc: configure(version, customHTTPClient), } } -func configure(version string) schema.ConfigureContextFunc { +func configure(version string, httpClient neon.HTTPClient) schema.ConfigureContextFunc { return func(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) { - if version == "dev" { - c, err := neon.NewClient(neon.Config{HTTPClient: neon.NewMockHTTPClient()}) - if err != nil { - return nil, diag.FromErr(err) - } - return c, diag.FromErr(err) + cfg := neon.Config{ + Key: d.Get("api_key").(string), } - c, err := neon.NewClient(neon.Config{Key: d.Get("api_key").(string)}) - if err != nil { - return nil, diag.FromErr(err) + + switch version { + case "dev": + cfg.HTTPClient = neon.NewMockHTTPClient() + + default: + if httpClient == nil { + // The default timeout is set arbitrary to ensure the connection is shut during resources state update + httpClient = &http.Client{Timeout: 2 * time.Minute} + } + + // Wrap the HTTP client into the client which injects the User-Agent header for tracing. + cfg.HTTPClient = httpClientWithUserAgent{httpClient: httpClient, providerVersion: version} } - return c, nil + + c, err := neon.NewClient(cfg) + + return c, diag.FromErr(err) + } +} + +type httpClientWithUserAgent struct { + httpClient neon.HTTPClient + + providerVersion string +} + +const DefaultApplicationName = "kislerdm/neon" + +func (h httpClientWithUserAgent) Do(r *http.Request) (*http.Response, error) { + if r.Header == nil { + r.Header = make(http.Header) } + r.Header.Set("User-Agent", fmt.Sprintf("tfProvider-%s@%s", DefaultApplicationName, h.providerVersion)) + return h.httpClient.Do(r) } diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go new file mode 100644 index 0000000..b929481 --- /dev/null +++ b/internal/provider/provider_test.go @@ -0,0 +1,63 @@ +//go:build !acceptance +// +build !acceptance + +package provider + +import ( + "io" + "net/http" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/stretchr/testify/assert" +) + +type httpClient struct { + header http.Header +} + +func (h *httpClient) Do(r *http.Request) (*http.Response, error) { + h.header = r.Header + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader("{}")), + }, nil +} + +func TestUserAgentInstrumentation(t *testing.T) { + const ( + wantVersion = "ProviderVer" + + resourceDefinition = `resource "neon_role" "this" { + project_id = "foo" + branch_id = "foo" + name = "foo" +}` + ) + + c := &httpClient{} + + t.Setenv("NEON_API_KEY", "foo") + + resource.UnitTest(t, resource.TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "neon": func() (*schema.Provider, error) { + return newWithClient(wantVersion, c), nil + }, + }, + Steps: []resource.TestStep{ + { + Config: resourceDefinition, + ExpectNonEmptyPlan: true, + }, + }, + }) + + userAgent := c.header.Get("User-Agent") + assert.Contains(t, userAgent, DefaultApplicationName) + els := strings.Split(userAgent, "@") + assert.Len(t, els, 2) + assert.Equal(t, wantVersion, els[1]) +} diff --git a/main.go b/main.go index 6b58c67..eb15eed 100644 --- a/main.go +++ b/main.go @@ -33,7 +33,7 @@ func main() { opts := &plugin.ServeOpts{ Debug: debugMode, - ProviderAddr: "registry.terraform.io/kislerdm/neon", + ProviderAddr: "registry.terraform.io/" + provider.DefaultApplicationName, ProviderFunc: func() *schema.Provider { return provider.New(version)