From 517790abefe34ccb6fd37dab790881e57dc7ebc3 Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Wed, 14 Oct 2026 22:20:15 +1100 Subject: [PATCH] provider: add bearer token auth --- README.md | 8 ++- examples/main.tf | 1 + internal/provider/client.go | 4 ++ internal/provider/provider.go | 11 +++ internal/provider/provider_token_test.go | 87 ++++++++++++++++++++++++ 5 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 internal/provider/provider_token_test.go diff --git a/README.md b/README.md index bcd3e03..4d9266e 100644 --- a/README.md +++ b/README.md @@ -37,12 +37,14 @@ terraform { provider "artifactapi" { endpoint = "https://artifactapi.example.com" + token = var.artifactapi_token # optional } ``` -| Attribute | Required | Description | -|------------|----------|--------------------------------------| -| `endpoint` | Yes | ArtifactAPI server endpoint URL | +| Attribute | Required | Description | +|------------|----------|------------------------------------------------------------------------------| +| `endpoint` | Yes | ArtifactAPI server endpoint URL | +| `token` | No | Bearer token sent as `Authorization` header; falls back to `ARTIFACTAPI_TOKEN` | ## Resources diff --git a/examples/main.tf b/examples/main.tf index 678f4e7..9ab4da2 100644 --- a/examples/main.tf +++ b/examples/main.tf @@ -9,6 +9,7 @@ terraform { provider "artifactapi" { endpoint = "https://artifactapi.k8s.syd1.au.unkin.net" + # token is read from ARTIFACTAPI_TOKEN when unset } # --------------------------------------------------------------------------- diff --git a/internal/provider/client.go b/internal/provider/client.go index 247e1f0..4bc630c 100644 --- a/internal/provider/client.go +++ b/internal/provider/client.go @@ -11,6 +11,7 @@ import ( type apiClient struct { baseURL string + token string httpClient *http.Client } @@ -54,6 +55,9 @@ func (c *apiClient) do(ctx context.Context, method, path string, body, out any) if body != nil { req.Header.Set("Content-Type", "application/json") } + if c.token != "" { + req.Header.Set("Authorization", "Bearer "+c.token) + } resp, err := c.httpClient.Do(req) if err != nil { diff --git a/internal/provider/provider.go b/internal/provider/provider.go index ce54a26..16e4a24 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -2,6 +2,7 @@ package provider import ( "context" + "os" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/provider" @@ -18,6 +19,7 @@ type ArtifactAPIProvider struct { type artifactAPIProviderModel struct { Endpoint types.String `tfsdk:"endpoint"` + Token types.String `tfsdk:"token"` } func New(version string) func() provider.Provider { @@ -39,6 +41,11 @@ func (p *ArtifactAPIProvider) Schema(_ context.Context, _ provider.SchemaRequest Description: "The ArtifactAPI server endpoint URL (e.g. https://artifactapi.example.com).", Required: true, }, + "token": schema.StringAttribute{ + Description: "Bearer token sent as the Authorization header. Falls back to the ARTIFACTAPI_TOKEN environment variable.", + Optional: true, + Sensitive: true, + }, }, } } @@ -51,6 +58,10 @@ func (p *ArtifactAPIProvider) Configure(ctx context.Context, req provider.Config } client := newAPIClient(config.Endpoint.ValueString()) + client.token = config.Token.ValueString() + if client.token == "" { + client.token = os.Getenv("ARTIFACTAPI_TOKEN") + } resp.DataSourceData = client resp.ResourceData = client } diff --git a/internal/provider/provider_token_test.go b/internal/provider/provider_token_test.go new file mode 100644 index 0000000..b5e4fdf --- /dev/null +++ b/internal/provider/provider_token_test.go @@ -0,0 +1,87 @@ +package provider + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + fwprovider "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func configuredClient(t *testing.T, endpoint string, token *string) *apiClient { + t.Helper() + p := &ArtifactAPIProvider{} + var schemaResp fwprovider.SchemaResponse + p.Schema(context.Background(), fwprovider.SchemaRequest{}, &schemaResp) + + tokenVal := tftypes.NewValue(tftypes.String, nil) + if token != nil { + tokenVal = tftypes.NewValue(tftypes.String, *token) + } + raw := tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "endpoint": tftypes.String, + "token": tftypes.String, + }}, map[string]tftypes.Value{ + "endpoint": tftypes.NewValue(tftypes.String, endpoint), + "token": tokenVal, + }) + + var resp fwprovider.ConfigureResponse + p.Configure(context.Background(), fwprovider.ConfigureRequest{ + Config: tfsdk.Config{Schema: schemaResp.Schema, Raw: raw}, + }, &resp) + if resp.Diagnostics.HasError() { + t.Fatalf("configure: %v", resp.Diagnostics) + } + return resp.ResourceData.(*apiClient) +} + +func TestProvider_BearerToken(t *testing.T) { + strp := func(s string) *string { return &s } + cases := []struct { + name string + attr *string + env string + want string + }{ + {"attribute", strp("attr-tok"), "", "Bearer attr-tok"}, + {"env fallback", nil, "env-tok", "Bearer env-tok"}, + {"attribute wins over env", strp("attr-tok"), "env-tok", "Bearer attr-tok"}, + {"unset", nil, "", ""}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Setenv("ARTIFACTAPI_TOKEN", tc.env) + var got []string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + got = append(got, r.Header.Get("Authorization")) + _, present := r.Header["Authorization"] + if tc.want == "" && present { + t.Errorf("unexpected Authorization header on %s", r.Method) + } + w.WriteHeader(http.StatusNoContent) + })) + defer srv.Close() + + c := configuredClient(t, srv.URL, tc.attr) + ctx := context.Background() + if err := c.get(ctx, "/x", nil); err != nil { + t.Fatal(err) + } + if err := c.put(ctx, "/x", map[string]string{"a": "b"}, nil); err != nil { + t.Fatal(err) + } + if err := c.del(ctx, "/x"); err != nil { + t.Fatal(err) + } + for _, h := range got { + if h != tc.want { + t.Errorf("Authorization = %q, want %q", h, tc.want) + } + } + }) + } +}