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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Requests now identify as the provider rather than the Go SDK it is built on:
`x-sdk-name: Terraform`, `x-sdk-language: terraform` and `x-sdk-version` set
to the provider version. The User-Agent now leads with the Terraform version
and keeps the Go SDK token after the provider's, so Appwrite can tell
Terraform traffic apart from direct Go SDK use
- Every argument that cannot be changed in place now says so in its
documentation, with the sentence "Changing this forces a new resource to be
created." 170 attributes were affected and none of them mentioned it, so the
Expand Down
19 changes: 0 additions & 19 deletions internal/common/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,25 +53,6 @@ type AppwriteClients struct {
OrganizationID string
}

// WithUserAgent returns a ClientOption that sets the User-Agent header to identify
// Terraform provider traffic. This is required for HashiCorp partner providers.
//
// TF_APPEND_USER_AGENT is appended when set. terraform-plugin-sdk honors that
// variable for free, but a framework-only provider has to do it itself, so it
// previously had no effect here -- and it is how Terraform Cloud, Terragrunt and
// in-house wrappers identify themselves. Without it their traffic looks the same
// as a developer's laptop in Appwrite's logs.
func WithUserAgent(version string) client.ClientOption {
return func(clt *client.Client) error {
userAgent := fmt.Sprintf("terraform-provider-appwrite/%s", version)
if appended := AppendedUserAgent(); appended != "" {
userAgent = userAgent + " " + appended
}
clt.Headers["user-agent"] = userAgent
return nil
}
}

// ClientForProject creates a new SDK client targeting a specific project.
func (ac *AppwriteClients) ClientForProject(projectID string) client.Client {
opts := make([]client.ClientOption, 0, len(ac.BaseOptions)+1)
Expand Down
49 changes: 49 additions & 0 deletions internal/common/identity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package common

import (
"strings"

"github.com/appwrite/sdk-for-go/v7/client"
)

// Values the provider reports in the SDK identity headers. Appwrite reads
// x-sdk-name to attribute API key usage and x-sdk-language to classify
// deployments, so Terraform traffic must not report itself as the Go SDK.
const (
SDKName = "Terraform"
SDKPlatform = "server"
SDKLanguage = "terraform"
)

// WithIdentity returns a ClientOption that identifies requests as coming from
// this provider rather than the Go SDK it is built on.
//
// The User-Agent follows the HashiCorp convention of Terraform core, then the
// provider, then the underlying client, so the SDK version stays traceable. The
// Terraform token is omitted when the version is unknown. TF_APPEND_USER_AGENT
// is appended when set.
//
// It must be applied through appwrite.NewClient, which sets the SDK's own
// User-Agent before running options.
func WithIdentity(providerVersion string, terraformVersion string) client.ClientOption {
return func(clt *client.Client) error {
tokens := make([]string, 0, 4)
if terraformVersion != "" {
tokens = append(tokens, "Terraform/"+terraformVersion)
}
tokens = append(tokens, "terraform-provider-appwrite/"+providerVersion)
if sdk := clt.Headers["user-agent"]; sdk != "" {
tokens = append(tokens, sdk)
}
if appended := AppendedUserAgent(); appended != "" {
tokens = append(tokens, appended)
}

clt.Headers["user-agent"] = strings.Join(tokens, " ")
clt.Headers["x-sdk-name"] = SDKName
clt.Headers["x-sdk-platform"] = SDKPlatform
clt.Headers["x-sdk-language"] = SDKLanguage
clt.Headers["x-sdk-version"] = providerVersion
return nil
}
}
88 changes: 88 additions & 0 deletions internal/common/identity_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package common

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

"github.com/appwrite/sdk-for-go/v7/appwrite"
)

// sentHeaders issues one request through an SDK client configured with
// WithIdentity and returns the headers Appwrite would have received.
func sentHeaders(t *testing.T, providerVersion string, terraformVersion string) http.Header {
t.Helper()

var received http.Header
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
received = r.Header.Clone()
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{}`))
}))
t.Cleanup(server.Close)

clt := appwrite.NewClient(
appwrite.WithEndpoint(server.URL),
WithIdentity(providerVersion, terraformVersion),
)
if _, err := clt.Call(http.MethodGet, "/health", nil, nil); err != nil {
t.Fatalf("request: %v", err)
}
if received == nil {
t.Fatal("the server received no request")
}
return received
}

// Requests used to report x-sdk-name "Go", so Appwrite attributed provider
// traffic to the Go SDK and only the User-Agent told them apart.
func TestWithIdentityReportsTerraform(t *testing.T) {
t.Setenv("TF_APPEND_USER_AGENT", "")

headers := sentHeaders(t, "2.1.0", "1.9.5")

if got := headers.Get("X-SDK-Name"); got != "Terraform" {
t.Errorf("x-sdk-name = %q, want Terraform", got)
}
if got := headers.Get("X-SDK-Language"); got != "terraform" {
t.Errorf("x-sdk-language = %q, want terraform", got)
}
if got := headers.Get("X-SDK-Platform"); got != "server" {
t.Errorf("x-sdk-platform = %q, want server", got)
}
if got := headers.Get("X-SDK-Version"); got != "2.1.0" {
t.Errorf("x-sdk-version = %q, want the provider version", got)
}
}

func TestWithIdentityUserAgent(t *testing.T) {
t.Setenv("TF_APPEND_USER_AGENT", "terragrunt/0.55.0")

userAgent := sentHeaders(t, "2.1.0", "1.9.5").Get("User-Agent")

if !strings.HasPrefix(userAgent, "Terraform/1.9.5 terraform-provider-appwrite/2.1.0 ") {
t.Errorf("user-agent = %q, want it to lead with Terraform core and then the provider", userAgent)
}
if !strings.Contains(userAgent, "AppwriteGoSDK/") {
t.Errorf("user-agent = %q, want the underlying SDK kept traceable", userAgent)
}
if !strings.HasSuffix(userAgent, " terragrunt/0.55.0") {
t.Errorf("user-agent = %q, want TF_APPEND_USER_AGENT last", userAgent)
}
}

// Terraform core does not always report its version; a blank one must not
// leave an empty "Terraform/" token.
func TestWithIdentityUserAgentWithoutTerraformVersion(t *testing.T) {
t.Setenv("TF_APPEND_USER_AGENT", "")

userAgent := sentHeaders(t, "2.1.0", "").Get("User-Agent")

if strings.Contains(userAgent, "Terraform/") {
t.Errorf("user-agent = %q, want no Terraform token", userAgent)
}
if !strings.HasPrefix(userAgent, "terraform-provider-appwrite/2.1.0 ") {
t.Errorf("user-agent = %q, want it to lead with the provider", userAgent)
}
}
8 changes: 3 additions & 5 deletions internal/common/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ type HTTPConfig struct {
Timeout time.Duration
SelfSigned bool
MaxRetries int
UserAgent string
}

// WithHTTPTransport installs the provider's transport chain on an SDK client.
Expand Down Expand Up @@ -375,10 +374,9 @@ func safeHeaders(h http.Header) map[string]string {
// AppendedUserAgent returns the value of TF_APPEND_USER_AGENT.
//
// terraform-plugin-sdk honors this variable for free; a framework-only provider
// has to do it itself, which is why it currently has no effect here. It is how
// Terraform Cloud, Terragrunt and in-house wrappers identify themselves, and
// without it their traffic is indistinguishable from a developer's laptop in
// Appwrite's logs.
// has to do it itself. It is how Terraform Cloud, Terragrunt and in-house
// wrappers identify themselves, and without it their traffic is
// indistinguishable from a developer's laptop in Appwrite's logs.
func AppendedUserAgent() string {
return strings.TrimSpace(os.Getenv("TF_APPEND_USER_AGENT"))
}
16 changes: 0 additions & 16 deletions internal/common/transport_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -556,22 +556,6 @@ func TestAppendedUserAgent(t *testing.T) {
}
}

func TestWithUserAgentAppendsEnvironment(t *testing.T) {
t.Setenv("TF_APPEND_USER_AGENT", "terragrunt/0.55.0")

clt := newTestClient()
if err := WithUserAgent("2.1.0")(&clt); err != nil {
t.Fatalf("WithUserAgent: %v", err)
}
got := clt.Headers["user-agent"]
if !strings.HasPrefix(got, "terraform-provider-appwrite/2.1.0") {
t.Errorf("user-agent = %q, want it to start with the provider and version", got)
}
if !strings.Contains(got, "terragrunt/0.55.0") {
t.Errorf("user-agent = %q, want TF_APPEND_USER_AGENT appended", got)
}
}

// newTestClient returns an SDK client shaped the way appwrite.NewClient leaves
// one: maps initialized, no transport yet.
func newTestClient() client.Client {
Expand Down
4 changes: 2 additions & 2 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,14 +181,14 @@ func (p *appwriteProvider) Configure(ctx context.Context, req provider.Configure
appwrite.WithEndpoint(endpoint),
appwrite.WithKey(apiKey),
appwrite.WithTimeout(httpTimeout),
common.WithUserAgent(p.version),
common.WithIdentity(p.version, req.TerraformVersion),
common.WithHTTPTransport(httpConfig),
}
organizationBaseOpts := []client.ClientOption{
appwrite.WithEndpoint(endpoint),
appwrite.WithKey(organizationAPIKey),
appwrite.WithTimeout(httpTimeout),
common.WithUserAgent(p.version),
common.WithIdentity(p.version, req.TerraformVersion),
common.WithHTTPTransport(httpConfig),
}

Expand Down
Loading