Skip to content

Commit 244f551

Browse files
committed
review changes 2
Signed-off-by: Mauritz Uphoff <[email protected]>
1 parent 03632d8 commit 244f551

File tree

10 files changed

+231
-47
lines changed

10 files changed

+231
-47
lines changed

docs/ephemeral-resources/access_token.md

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
page_title: "stackit_access_token Ephemeral Resource - stackit"
44
subcategory: ""
55
description: |-
6-
STACKIT Access Token ephemeral resource schema.
6+
Ephemeral resource that generates a short-lived STACKIT access token (JWT) using a service account key. A new token is generated each time the resource is evaluated, and it remains consistent for the duration of a Terraform operation. If a private key is not explicitly provided, the provider attempts to extract it from the service account key instead. Token generation logic prioritizes environment variables first, followed by provider configuration. Access tokens generated from service account keys expire after 60 minutes.
77
---
88

99
# stackit_access_token (Ephemeral Resource)
1010

11-
STACKIT Access Token ephemeral resource schema.
11+
Ephemeral resource that generates a short-lived STACKIT access token (JWT) using a service account key. A new token is generated each time the resource is evaluated, and it remains consistent for the duration of a Terraform operation. If a private key is not explicitly provided, the provider attempts to extract it from the service account key instead. Token generation logic prioritizes environment variables first, followed by provider configuration. Access tokens generated from service account keys expire after 60 minutes.
1212

1313
## Example Usage
1414

@@ -17,17 +17,35 @@ ephemeral "stackit_access_token" "example" {}
1717
1818
// https://registry.terraform.io/providers/Mastercard/restapi/latest/docs
1919
provider "restapi" {
20-
alias = "stackit_iaas"
21-
uri = "https://iaas.api.eu01.stackit.cloud"
20+
uri = "https://iaas.api.eu01.stackit.cloud/"
2221
write_returns_object = true
2322
2423
headers = {
25-
"Authorization" = "Bearer ${ephemeral.stackit_access_token.example.access_token}"
24+
Authorization = "Bearer ${ephemeral.stackit_access_token.example.access_token}"
25+
Content-Type = "application/json"
2626
}
2727
28-
create_method = "GET"
29-
update_method = "GET"
30-
destroy_method = "GET"
28+
create_method = "POST"
29+
update_method = "PUT"
30+
destroy_method = "DELETE"
31+
}
32+
33+
resource "restapi_object" "iaas_keypair" {
34+
path = "/v2/keypairs"
35+
36+
data = jsonencode({
37+
labels = {
38+
key = "testvalue"
39+
}
40+
name = "test-keypair-123"
41+
publicKey = file(chomp("~/.ssh/id_rsa.pub"))
42+
})
43+
44+
id_attribute = "name"
45+
read_method = "GET"
46+
create_method = "POST"
47+
update_method = "PATCH"
48+
destroy_method = "DELETE"
3149
}
3250
```
3351

examples/ephemeral-resources/stackit_access_token/ephemeral-resource.tf

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,33 @@ ephemeral "stackit_access_token" "example" {}
22

33
// https://registry.terraform.io/providers/Mastercard/restapi/latest/docs
44
provider "restapi" {
5-
alias = "stackit_iaas"
6-
uri = "https://iaas.api.eu01.stackit.cloud"
5+
uri = "https://iaas.api.eu01.stackit.cloud/"
76
write_returns_object = true
87

98
headers = {
10-
"Authorization" = "Bearer ${ephemeral.stackit_access_token.example.access_token}"
9+
Authorization = "Bearer ${ephemeral.stackit_access_token.example.access_token}"
10+
Content-Type = "application/json"
1111
}
1212

13-
create_method = "GET"
14-
update_method = "GET"
15-
destroy_method = "GET"
16-
}
13+
create_method = "POST"
14+
update_method = "PUT"
15+
destroy_method = "DELETE"
16+
}
17+
18+
resource "restapi_object" "iaas_keypair" {
19+
path = "/v2/keypairs"
20+
21+
data = jsonencode({
22+
labels = {
23+
key = "testvalue"
24+
}
25+
name = "test-keypair-123"
26+
publicKey = file(chomp("~/.ssh/id_rsa.pub"))
27+
})
28+
29+
id_attribute = "name"
30+
read_method = "GET"
31+
create_method = "POST"
32+
update_method = "PATCH"
33+
destroy_method = "DELETE"
34+
}

stackit/internal/conversion/conversion.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,6 @@ func ParseProviderData(ctx context.Context, providerData any, diags *diag.Diagno
184184
return stackitProviderData, true
185185
}
186186

187-
// TODO: write tests
188187
func ParseEphemeralProviderData(ctx context.Context, providerData any, diags *diag.Diagnostics) (core.EphemeralProviderData, bool) {
189188
// Prevent panic if the provider has not been configured.
190189
if providerData == nil {

stackit/internal/conversion/conversion_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,3 +304,91 @@ func TestParseProviderData(t *testing.T) {
304304
})
305305
}
306306
}
307+
308+
func TestParseEphemeralProviderData(t *testing.T) {
309+
type args struct {
310+
providerData any
311+
}
312+
type want struct {
313+
ok bool
314+
providerData core.EphemeralProviderData
315+
}
316+
tests := []struct {
317+
name string
318+
args args
319+
want want
320+
wantErr bool
321+
}{
322+
{
323+
name: "provider has not been configured",
324+
args: args{
325+
providerData: nil,
326+
},
327+
want: want{
328+
ok: false,
329+
},
330+
wantErr: false,
331+
},
332+
{
333+
name: "invalid provider data",
334+
args: args{
335+
providerData: struct{}{},
336+
},
337+
want: want{
338+
ok: false,
339+
},
340+
wantErr: true,
341+
},
342+
{
343+
name: "valid provider data 1",
344+
args: args{
345+
providerData: core.EphemeralProviderData{},
346+
},
347+
want: want{
348+
ok: true,
349+
providerData: core.EphemeralProviderData{},
350+
},
351+
wantErr: false,
352+
},
353+
{
354+
name: "valid provider data 2",
355+
args: args{
356+
providerData: core.EphemeralProviderData{
357+
PrivateKey: "",
358+
PrivateKeyPath: "/home/dev/foo/private-key.json",
359+
ServiceAccountKey: "",
360+
ServiceAccountKeyPath: "/home/dev/foo/key.json",
361+
TokenCustomEndpoint: "",
362+
},
363+
},
364+
want: want{
365+
ok: true,
366+
providerData: core.EphemeralProviderData{
367+
PrivateKey: "",
368+
PrivateKeyPath: "/home/dev/foo/private-key.json",
369+
ServiceAccountKey: "",
370+
ServiceAccountKeyPath: "/home/dev/foo/key.json",
371+
TokenCustomEndpoint: "",
372+
},
373+
},
374+
wantErr: false,
375+
},
376+
}
377+
for _, tt := range tests {
378+
t.Run(tt.name, func(t *testing.T) {
379+
ctx := context.Background()
380+
diags := diag.Diagnostics{}
381+
382+
actual, ok := ParseEphemeralProviderData(ctx, tt.args.providerData, &diags)
383+
if diags.HasError() != tt.wantErr {
384+
t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr)
385+
}
386+
if ok != tt.want.ok {
387+
t.Errorf("ParseProviderData() got = %v, want %v", ok, tt.want.ok)
388+
}
389+
if !reflect.DeepEqual(actual, tt.want.providerData) {
390+
t.Errorf("ParseProviderData() got = %v, want %v", actual, tt.want)
391+
}
392+
})
393+
}
394+
}

stackit/internal/core/core.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ const (
2626
)
2727

2828
type EphemeralProviderData struct {
29-
ProviderData
30-
3129
PrivateKey string
3230
PrivateKeyPath string
3331
ServiceAccountKey string
@@ -38,7 +36,6 @@ type EphemeralProviderData struct {
3836
type ProviderData struct {
3937
RoundTripper http.RoundTripper
4038
ServiceAccountEmail string // Deprecated: ServiceAccountEmail is not required and will be removed after 12th June 2025.
41-
4239
// Deprecated: Use DefaultRegion instead
4340
Region string
4441
DefaultRegion string
Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,50 @@
1-
package access_token
1+
package access_token_test
2+
3+
import (
4+
_ "embed"
5+
"regexp"
6+
"testing"
7+
8+
"github.com/hashicorp/terraform-plugin-testing/config"
9+
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
10+
"github.com/hashicorp/terraform-plugin-testing/knownvalue"
11+
"github.com/hashicorp/terraform-plugin-testing/statecheck"
12+
"github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
13+
"github.com/hashicorp/terraform-plugin-testing/tfversion"
14+
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil"
15+
)
16+
17+
//go:embed testdata/ephemeral_resource.tf
18+
var ephemeralResourceConfig string
19+
20+
var testConfigVars = config.Variables{
21+
"default_region": config.StringVariable(testutil.Region),
22+
}
23+
24+
func TestAccEphemeralAccessToken(t *testing.T) {
25+
resource.Test(t, resource.TestCase{
26+
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
27+
tfversion.SkipBelow(tfversion.Version1_10_0),
28+
},
29+
ProtoV6ProviderFactories: testutil.TestEphemeralAccProtoV6ProviderFactories,
30+
Steps: []resource.TestStep{
31+
{
32+
Config: ephemeralResourceConfig,
33+
ConfigVariables: testConfigVars,
34+
ConfigStateChecks: []statecheck.StateCheck{
35+
statecheck.ExpectKnownValue(
36+
"echo.example",
37+
tfjsonpath.New("data").AtMapKey("access_token"),
38+
knownvalue.NotNull(),
39+
),
40+
// JWT access tokens start with "ey" because the first part is base64-encoded JSON that begins with "{".
41+
statecheck.ExpectKnownValue(
42+
"echo.example",
43+
tfjsonpath.New("data").AtMapKey("access_token"),
44+
knownvalue.StringRegexp(regexp.MustCompile(`^ey`)),
45+
),
46+
},
47+
},
48+
},
49+
})
50+
}

stackit/internal/services/access_token/ephemeral_resource.go

Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,7 @@ func NewAccessTokenEphemeralResource() ephemeral.EphemeralResource {
2424
}
2525

2626
type accessTokenEphemeralResource struct {
27-
serviceAccountKeyPath string
28-
serviceAccountKey string
29-
privateKeyPath string
30-
privateKey string
31-
tokenCustomEndpoint string
27+
keyAuthConfig config.Configuration
3228
}
3329

3430
func (e *accessTokenEphemeralResource) Configure(ctx context.Context, req ephemeral.ConfigureRequest, resp *ephemeral.ConfigureResponse) {
@@ -37,11 +33,13 @@ func (e *accessTokenEphemeralResource) Configure(ctx context.Context, req epheme
3733
return
3834
}
3935

40-
e.serviceAccountKey = providerData.ServiceAccountKey
41-
e.serviceAccountKeyPath = providerData.ServiceAccountKeyPath
42-
e.privateKey = providerData.PrivateKey
43-
e.privateKeyPath = providerData.PrivateKeyPath
44-
e.tokenCustomEndpoint = providerData.TokenCustomEndpoint
36+
e.keyAuthConfig = config.Configuration{
37+
ServiceAccountKey: providerData.ServiceAccountKey,
38+
ServiceAccountKeyPath: providerData.ServiceAccountKeyPath,
39+
PrivateKeyPath: providerData.PrivateKey,
40+
PrivateKey: providerData.PrivateKeyPath,
41+
TokenCustomUrl: providerData.TokenCustomEndpoint,
42+
}
4543
}
4644

4745
type ephemeralTokenModel struct {
@@ -54,7 +52,11 @@ func (e *accessTokenEphemeralResource) Metadata(_ context.Context, req ephemeral
5452

5553
func (e *accessTokenEphemeralResource) Schema(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) {
5654
resp.Schema = schema.Schema{
57-
Description: "STACKIT Access Token ephemeral resource schema.",
55+
Description: "Ephemeral resource that generates a short-lived STACKIT access token (JWT) using a service account key. " +
56+
"A new token is generated each time the resource is evaluated, and it remains consistent for the duration of a Terraform operation. " +
57+
"If a private key is not explicitly provided, the provider attempts to extract it from the service account key instead. " +
58+
"Token generation logic prioritizes environment variables first, followed by provider configuration. " +
59+
"Access tokens generated from service account keys expire after 60 minutes.",
5860
Attributes: map[string]schema.Attribute{
5961
"access_token": schema.StringAttribute{
6062
Description: "JWT access token for STACKIT API authentication.",
@@ -73,15 +75,7 @@ func (e *accessTokenEphemeralResource) Open(ctx context.Context, req ephemeral.O
7375
return
7476
}
7577

76-
cfg := config.Configuration{
77-
ServiceAccountKey: e.serviceAccountKey,
78-
ServiceAccountKeyPath: e.serviceAccountKeyPath,
79-
PrivateKeyPath: e.privateKeyPath,
80-
PrivateKey: e.privateKey,
81-
TokenCustomUrl: e.tokenCustomEndpoint,
82-
}
83-
84-
rt, err := auth.KeyAuth(&cfg)
78+
rt, err := auth.KeyAuth(&e.keyAuthConfig)
8579
if err != nil {
8680
core.LogAndAddError(ctx, &resp.Diagnostics, "Access token generation failed", fmt.Sprintf("Failed to initialize authentication: %v", err))
8781
return
@@ -97,12 +91,7 @@ func (e *accessTokenEphemeralResource) Open(ctx context.Context, req ephemeral.O
9791
// Retrieve the access token
9892
accessToken, err := client.GetAccessToken()
9993
if err != nil {
100-
core.LogAndAddError(
101-
ctx,
102-
&resp.Diagnostics,
103-
"Access token retrieval failed",
104-
fmt.Sprintf("Error obtaining access token: %v", err),
105-
)
94+
core.LogAndAddError(ctx, &resp.Diagnostics, "Access token retrieval failed", fmt.Sprintf("Error obtaining access token: %v", err))
10695
return
10796
}
10897

stackit/internal/services/access_token/ephemeral_resource_test.go

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
variable "default_region" {}
2+
3+
provider "stackit" {
4+
default_region = var.default_region
5+
}
6+
7+
ephemeral "stackit_access_token" "example" {}
8+
9+
provider "echo" {
10+
data = ephemeral.stackit_access_token.example
11+
}
12+
13+
resource "echo" "example" {
14+
}

stackit/internal/testutil/testutil.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/hashicorp/terraform-plugin-framework/providerserver"
1212
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
1313
"github.com/hashicorp/terraform-plugin-testing/config"
14+
"github.com/hashicorp/terraform-plugin-testing/echoprovider"
1415

1516
"github.com/stackitcloud/terraform-provider-stackit/stackit"
1617
)
@@ -29,6 +30,18 @@ var (
2930
"stackit": providerserver.NewProtocol6WithError(stackit.New("test-version")()),
3031
}
3132

33+
// TestEphemeralAccProtoV6ProviderFactories is used to instantiate a provider during
34+
// acceptance testing. The factory function will be invoked for every Terraform
35+
// CLI command executed to create a provider server to which the CLI can
36+
// reattach.
37+
//
38+
// See the Terraform acceptance test documentation on ephemeral resources for more information:
39+
// https://developer.hashicorp.com/terraform/plugin/testing/acceptance-tests/ephemeral-resources
40+
TestEphemeralAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){
41+
"stackit": providerserver.NewProtocol6WithError(stackit.New("test-version")()),
42+
"echo": echoprovider.NewProviderServer(),
43+
}
44+
3245
// E2ETestsEnabled checks if end-to-end tests should be run.
3346
// It is enabled when the TF_ACC environment variable is set to "1".
3447
E2ETestsEnabled = os.Getenv("TF_ACC") == "1"

0 commit comments

Comments
 (0)