feat(access-token): add ephemeral access-token resource (#1068)

* feat(access-token): add ephemeral access-token resource

Signed-off-by: Mauritz Uphoff <mauritz.uphoff@stackit.cloud>
This commit is contained in:
Mauritz Uphoff 2025-12-03 10:13:28 +01:00 committed by GitHub
parent 368b8d55be
commit 0e9b97a513
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 733 additions and 5 deletions

View file

@ -0,0 +1,73 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "stackit_access_token Ephemeral Resource - stackit"
subcategory: ""
description: |-
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. Access tokens generated from service account keys expire after 60 minutes.
~> Service account key credentials must be configured either in the STACKIT provider configuration or via environment variables (see example below). If any other authentication method is configured, this ephemeral resource will fail with an error.
~> This ephemeral-resource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources.
---
# stackit_access_token (Ephemeral Resource)
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. Access tokens generated from service account keys expire after 60 minutes.
~> Service account key credentials must be configured either in the STACKIT provider configuration or via environment variables (see example below). If any other authentication method is configured, this ephemeral resource will fail with an error.
~> This ephemeral-resource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources.
## Example Usage
```terraform
provider "stackit" {
default_region = "eu01"
service_account_key_path = "/path/to/sa_key.json"
enable_beta_resources = true
}
ephemeral "stackit_access_token" "example" {}
locals {
stackit_api_base_url = "https://iaas.api.stackit.cloud"
public_ip_path = "/v2/projects/${var.project_id}/regions/${var.region}/public-ips"
public_ip_payload = {
labels = {
key = "value"
}
}
}
# Docs: https://registry.terraform.io/providers/Mastercard/restapi/latest
provider "restapi" {
uri = local.stackit_api_base_url
write_returns_object = true
headers = {
Authorization = "Bearer ${ephemeral.stackit_access_token.example.access_token}"
Content-Type = "application/json"
}
create_method = "POST"
update_method = "PATCH"
destroy_method = "DELETE"
}
resource "restapi_object" "public_ip_restapi" {
path = local.public_ip_path
data = jsonencode(local.public_ip_payload)
id_attribute = "id"
read_method = "GET"
create_method = "POST"
update_method = "PATCH"
destroy_method = "DELETE"
}
```
<!-- schema generated by tfplugindocs -->
## Schema
### Read-Only
- `access_token` (String, Sensitive) JWT access token for STACKIT API authentication.

View file

@ -0,0 +1,44 @@
provider "stackit" {
default_region = "eu01"
service_account_key_path = "/path/to/sa_key.json"
enable_beta_resources = true
}
ephemeral "stackit_access_token" "example" {}
locals {
stackit_api_base_url = "https://iaas.api.stackit.cloud"
public_ip_path = "/v2/projects/${var.project_id}/regions/${var.region}/public-ips"
public_ip_payload = {
labels = {
key = "value"
}
}
}
# Docs: https://registry.terraform.io/providers/Mastercard/restapi/latest
provider "restapi" {
uri = local.stackit_api_base_url
write_returns_object = true
headers = {
Authorization = "Bearer ${ephemeral.stackit_access_token.example.access_token}"
Content-Type = "application/json"
}
create_method = "POST"
update_method = "PATCH"
destroy_method = "DELETE"
}
resource "restapi_object" "public_ip_restapi" {
path = local.public_ip_path
data = jsonencode(local.public_ip_payload)
id_attribute = "id"
read_method = "GET"
create_method = "POST"
update_method = "PATCH"
destroy_method = "DELETE"
}

View file

@ -178,8 +178,22 @@ func ParseProviderData(ctx context.Context, providerData any, diags *diag.Diagno
stackitProviderData, ok := providerData.(core.ProviderData) stackitProviderData, ok := providerData.(core.ProviderData)
if !ok { if !ok {
core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", providerData)) core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Expected configure type core.ProviderData, got %T", providerData))
return core.ProviderData{}, false return core.ProviderData{}, false
} }
return stackitProviderData, true return stackitProviderData, true
} }
func ParseEphemeralProviderData(ctx context.Context, providerData any, diags *diag.Diagnostics) (core.EphemeralProviderData, bool) {
// Prevent panic if the provider has not been configured.
if providerData == nil {
return core.EphemeralProviderData{}, false
}
stackitProviderData, ok := providerData.(core.EphemeralProviderData)
if !ok {
core.LogAndAddError(ctx, diags, "Error configuring API client", "Expected configure type core.EphemeralProviderData")
return core.EphemeralProviderData{}, false
}
return stackitProviderData, true
}

View file

@ -304,3 +304,91 @@ func TestParseProviderData(t *testing.T) {
}) })
} }
} }
func TestParseEphemeralProviderData(t *testing.T) {
type args struct {
providerData any
}
type want struct {
ok bool
providerData core.EphemeralProviderData
}
tests := []struct {
name string
args args
want want
wantErr bool
}{
{
name: "provider has not been configured",
args: args{
providerData: nil,
},
want: want{
ok: false,
},
wantErr: false,
},
{
name: "invalid provider data",
args: args{
providerData: struct{}{},
},
want: want{
ok: false,
},
wantErr: true,
},
{
name: "valid provider data 1",
args: args{
providerData: core.EphemeralProviderData{},
},
want: want{
ok: true,
providerData: core.EphemeralProviderData{},
},
wantErr: false,
},
{
name: "valid provider data 2",
args: args{
providerData: core.EphemeralProviderData{
PrivateKey: "",
PrivateKeyPath: "/home/dev/foo/private-key.json",
ServiceAccountKey: "",
ServiceAccountKeyPath: "/home/dev/foo/key.json",
TokenCustomEndpoint: "",
},
},
want: want{
ok: true,
providerData: core.EphemeralProviderData{
PrivateKey: "",
PrivateKeyPath: "/home/dev/foo/private-key.json",
ServiceAccountKey: "",
ServiceAccountKeyPath: "/home/dev/foo/key.json",
TokenCustomEndpoint: "",
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
diags := diag.Diagnostics{}
actual, ok := ParseEphemeralProviderData(ctx, tt.args.providerData, &diags)
if diags.HasError() != tt.wantErr {
t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr)
}
if ok != tt.want.ok {
t.Errorf("ParseProviderData() got = %v, want %v", ok, tt.want.ok)
}
if !reflect.DeepEqual(actual, tt.want.providerData) {
t.Errorf("ParseProviderData() got = %v, want %v", actual, tt.want)
}
})
}
}

View file

@ -16,8 +16,9 @@ import (
type ResourceType string type ResourceType string
const ( const (
Resource ResourceType = "resource" Resource ResourceType = "resource"
Datasource ResourceType = "datasource" Datasource ResourceType = "datasource"
EphemeralResource ResourceType = "ephemeral-resource"
// Separator used for concatenation of TF-internal resource ID // Separator used for concatenation of TF-internal resource ID
Separator = "," Separator = ","
@ -26,6 +27,16 @@ const (
DatasourceRegionFallbackDocstring = "Uses the `default_region` specified in the provider configuration as a fallback in case no `region` is defined on datasource level." DatasourceRegionFallbackDocstring = "Uses the `default_region` specified in the provider configuration as a fallback in case no `region` is defined on datasource level."
) )
type EphemeralProviderData struct {
ProviderData
PrivateKey string
PrivateKeyPath string
ServiceAccountKey string
ServiceAccountKeyPath string
TokenCustomEndpoint string
}
type ProviderData struct { type ProviderData struct {
RoundTripper http.RoundTripper RoundTripper http.RoundTripper
ServiceAccountEmail string // Deprecated: ServiceAccountEmail is not required and will be removed after 12th June 2025. ServiceAccountEmail string // Deprecated: ServiceAccountEmail is not required and will be removed after 12th June 2025.

View file

@ -0,0 +1,50 @@
package access_token_test
import (
_ "embed"
"regexp"
"testing"
"github.com/hashicorp/terraform-plugin-testing/config"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/knownvalue"
"github.com/hashicorp/terraform-plugin-testing/statecheck"
"github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
"github.com/hashicorp/terraform-plugin-testing/tfversion"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil"
)
//go:embed testdata/ephemeral_resource.tf
var ephemeralResourceConfig string
var testConfigVars = config.Variables{
"default_region": config.StringVariable(testutil.Region),
}
func TestAccEphemeralAccessToken(t *testing.T) {
resource.Test(t, resource.TestCase{
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(tfversion.Version1_10_0),
},
ProtoV6ProviderFactories: testutil.TestEphemeralAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: ephemeralResourceConfig,
ConfigVariables: testConfigVars,
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue(
"echo.example",
tfjsonpath.New("data").AtMapKey("access_token"),
knownvalue.NotNull(),
),
// JWT access tokens start with "ey" because the first part is base64-encoded JSON that begins with "{".
statecheck.ExpectKnownValue(
"echo.example",
tfjsonpath.New("data").AtMapKey("access_token"),
knownvalue.StringRegexp(regexp.MustCompile(`^ey`)),
),
},
},
},
})
}

View file

@ -0,0 +1,132 @@
package access_token
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
"github.com/hashicorp/terraform-plugin-framework/ephemeral/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/stackitcloud/stackit-sdk-go/core/auth"
"github.com/stackitcloud/stackit-sdk-go/core/clients"
"github.com/stackitcloud/stackit-sdk-go/core/config"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features"
)
var (
_ ephemeral.EphemeralResource = &accessTokenEphemeralResource{}
_ ephemeral.EphemeralResourceWithConfigure = &accessTokenEphemeralResource{}
)
func NewAccessTokenEphemeralResource() ephemeral.EphemeralResource {
return &accessTokenEphemeralResource{}
}
type accessTokenEphemeralResource struct {
keyAuthConfig config.Configuration
}
func (e *accessTokenEphemeralResource) Configure(ctx context.Context, req ephemeral.ConfigureRequest, resp *ephemeral.ConfigureResponse) {
ephemeralProviderData, ok := conversion.ParseEphemeralProviderData(ctx, req.ProviderData, &resp.Diagnostics)
if !ok {
return
}
features.CheckBetaResourcesEnabled(
ctx,
&ephemeralProviderData.ProviderData,
&resp.Diagnostics,
"stackit_access_token", "ephemeral_resource",
)
if resp.Diagnostics.HasError() {
return
}
e.keyAuthConfig = config.Configuration{
ServiceAccountKey: ephemeralProviderData.ServiceAccountKey,
ServiceAccountKeyPath: ephemeralProviderData.ServiceAccountKeyPath,
PrivateKeyPath: ephemeralProviderData.PrivateKey,
PrivateKey: ephemeralProviderData.PrivateKeyPath,
TokenCustomUrl: ephemeralProviderData.TokenCustomEndpoint,
}
}
type ephemeralTokenModel struct {
AccessToken types.String `tfsdk:"access_token"`
}
func (e *accessTokenEphemeralResource) Metadata(_ context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_access_token"
}
func (e *accessTokenEphemeralResource) Schema(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) {
description := features.AddBetaDescription(
fmt.Sprintf(
"%s\n\n%s",
"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. "+
"Access tokens generated from service account keys expire after 60 minutes.",
"~> Service account key credentials must be configured either in the STACKIT provider configuration or via environment variables (see example below). "+
"If any other authentication method is configured, this ephemeral resource will fail with an error.",
),
core.EphemeralResource,
)
resp.Schema = schema.Schema{
Description: description,
Attributes: map[string]schema.Attribute{
"access_token": schema.StringAttribute{
Description: "JWT access token for STACKIT API authentication.",
Computed: true,
Sensitive: true,
},
},
}
}
func (e *accessTokenEphemeralResource) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) {
var model ephemeralTokenModel
resp.Diagnostics.Append(req.Config.Get(ctx, &model)...)
if resp.Diagnostics.HasError() {
return
}
accessToken, err := getAccessToken(&e.keyAuthConfig)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Access token generation failed", err.Error())
return
}
model.AccessToken = types.StringValue(accessToken)
resp.Diagnostics.Append(resp.Result.Set(ctx, model)...)
}
// getAccessToken initializes authentication using the provided config and returns an access token via the KeyFlow mechanism.
func getAccessToken(keyAuthConfig *config.Configuration) (string, error) {
roundTripper, err := auth.KeyAuth(keyAuthConfig)
if err != nil {
return "", fmt.Errorf(
"failed to initialize authentication: %w. "+
"Make sure service account credentials are configured either in the provider configuration or via environment variables",
err,
)
}
// Type assert to access token functionality
client, ok := roundTripper.(*clients.KeyFlow)
if !ok {
return "", fmt.Errorf("internal error: expected *clients.KeyFlow, but received a different implementation of http.RoundTripper")
}
// Retrieve the access token
accessToken, err := client.GetAccessToken()
if err != nil {
return "", fmt.Errorf("error obtaining access token: %w", err)
}
return accessToken, nil
}

View file

@ -0,0 +1,253 @@
package access_token
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
_ "embed"
"encoding/json"
"encoding/pem"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
"github.com/stackitcloud/stackit-sdk-go/core/clients"
"github.com/stackitcloud/stackit-sdk-go/core/config"
)
//go:embed testdata/service_account.json
var testServiceAccountKey string
func startMockTokenServer() *httptest.Server {
handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
resp := clients.TokenResponseBody{
AccessToken: "mock_access_token",
RefreshToken: "mock_refresh_token",
TokenType: "Bearer",
ExpiresIn: int(time.Now().Add(time.Hour).Unix()),
Scope: "mock_scope",
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(resp)
})
return httptest.NewServer(handler)
}
func generatePrivateKey() (string, error) {
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return "", err
}
privateKeyPEM := &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
}
return string(pem.EncodeToMemory(privateKeyPEM)), nil
}
func writeTempPEMFile(t *testing.T, pemContent string) string {
t.Helper()
tmpFile, err := os.CreateTemp("", "stackit_test_private_key_*.pem")
if err != nil {
t.Fatal(err)
}
if _, err := tmpFile.WriteString(pemContent); err != nil {
t.Fatal(err)
}
if err := tmpFile.Close(); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
_ = os.Remove(tmpFile.Name())
})
return tmpFile.Name()
}
func TestGetAccessToken(t *testing.T) {
mockServer := startMockTokenServer()
t.Cleanup(mockServer.Close)
privateKey, err := generatePrivateKey()
if err != nil {
t.Fatal(err)
}
tests := []struct {
description string
setupEnv func()
cleanupEnv func()
cfgFactory func() *config.Configuration
expectError bool
expected string
}{
{
description: "should return token when service account key passed by value",
cfgFactory: func() *config.Configuration {
return &config.Configuration{
ServiceAccountKey: testServiceAccountKey,
PrivateKey: privateKey,
TokenCustomUrl: mockServer.URL,
}
},
expectError: false,
expected: "mock_access_token",
},
{
description: "should return token when service account key is loaded from file path",
cfgFactory: func() *config.Configuration {
return &config.Configuration{
ServiceAccountKeyPath: "testdata/service_account.json",
PrivateKey: privateKey,
TokenCustomUrl: mockServer.URL,
}
},
expectError: false,
expected: "mock_access_token",
},
{
description: "should fail when private key is invalid",
cfgFactory: func() *config.Configuration {
return &config.Configuration{
ServiceAccountKey: "invalid-json",
PrivateKey: "invalid-PEM",
TokenCustomUrl: mockServer.URL,
}
},
expectError: true,
expected: "",
},
{
description: "should return token when service account key is set via env",
setupEnv: func() {
_ = os.Setenv("STACKIT_SERVICE_ACCOUNT_KEY", testServiceAccountKey)
},
cleanupEnv: func() {
_ = os.Unsetenv("STACKIT_SERVICE_ACCOUNT_KEY")
},
cfgFactory: func() *config.Configuration {
return &config.Configuration{
PrivateKey: privateKey,
TokenCustomUrl: mockServer.URL,
}
},
expectError: false,
expected: "mock_access_token",
},
{
description: "should return token when service account key path is set via env",
setupEnv: func() {
_ = os.Setenv("STACKIT_SERVICE_ACCOUNT_KEY_PATH", "testdata/service_account.json")
},
cleanupEnv: func() {
_ = os.Unsetenv("STACKIT_SERVICE_ACCOUNT_KEY_PATH")
},
cfgFactory: func() *config.Configuration {
return &config.Configuration{
PrivateKey: privateKey,
TokenCustomUrl: mockServer.URL,
}
},
expectError: false,
expected: "mock_access_token",
},
{
description: "should return token when private key is set via env",
setupEnv: func() {
_ = os.Setenv("STACKIT_PRIVATE_KEY", privateKey)
},
cleanupEnv: func() {
_ = os.Unsetenv("STACKIT_PRIVATE_KEY")
},
cfgFactory: func() *config.Configuration {
return &config.Configuration{
ServiceAccountKey: testServiceAccountKey,
TokenCustomUrl: mockServer.URL,
}
},
expectError: false,
expected: "mock_access_token",
},
{
description: "should return token when private key path is set via env",
setupEnv: func() {
// Write temp file and set env
tmpFile := writeTempPEMFile(t, privateKey)
_ = os.Setenv("STACKIT_PRIVATE_KEY_PATH", tmpFile)
},
cleanupEnv: func() {
_ = os.Unsetenv("STACKIT_PRIVATE_KEY_PATH")
},
cfgFactory: func() *config.Configuration {
return &config.Configuration{
ServiceAccountKey: testServiceAccountKey,
TokenCustomUrl: mockServer.URL,
}
},
expectError: false,
expected: "mock_access_token",
},
{
description: "should fail when no service account key or private key is set",
cfgFactory: func() *config.Configuration {
return &config.Configuration{
TokenCustomUrl: mockServer.URL,
}
},
expectError: true,
expected: "",
},
{
description: "should fail when no service account key or private key is set via env",
setupEnv: func() {
_ = os.Unsetenv("STACKIT_SERVICE_ACCOUNT_KEY")
_ = os.Unsetenv("STACKIT_SERVICE_ACCOUNT_KEY_PATH")
_ = os.Unsetenv("STACKIT_PRIVATE_KEY")
_ = os.Unsetenv("STACKIT_PRIVATE_KEY_PATH")
},
cleanupEnv: func() {
// Restore original environment variables
},
cfgFactory: func() *config.Configuration {
return &config.Configuration{
TokenCustomUrl: mockServer.URL,
}
},
expectError: true,
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
if tt.setupEnv != nil {
tt.setupEnv()
}
if tt.cleanupEnv != nil {
defer tt.cleanupEnv()
}
cfg := tt.cfgFactory()
token, err := getAccessToken(cfg)
if tt.expectError {
if err == nil {
t.Errorf("expected error but got none for test case '%s'", tt.description)
}
} else {
if err != nil {
t.Errorf("did not expect error but got: %v for test case '%s'", err, tt.description)
}
if token != tt.expected {
t.Errorf("expected token '%s', got '%s' for test case '%s'", tt.expected, token, tt.description)
}
}
})
}
}

View file

@ -0,0 +1,15 @@
variable "default_region" {}
provider "stackit" {
default_region = var.default_region
enable_beta_resources = true
}
ephemeral "stackit_access_token" "example" {}
provider "echo" {
data = ephemeral.stackit_access_token.example
}
resource "echo" "example" {
}

View file

@ -0,0 +1,16 @@
{
"id": "cad1592f-1fe6-4fd1-a6d6-ccef94b01697",
"publicKey": "-----BEGIN PUBLIC KEY-----\nABC\n-----END PUBLIC KEY-----",
"createdAt": "2025-11-25T15:19:30.689+00:00",
"keyType": "USER_MANAGED",
"keyOrigin": "GENERATED",
"keyAlgorithm": "RSA_2048",
"active": true,
"credentials": {
"kid": "cad1592f-1fe6-4fd1-a6d6-ccef94b01697",
"iss": "foo.bar@sa.stackit.cloud",
"sub": "cad1592f-1fe6-4fd1-a6d6-ccef94b01697",
"aud": "https://stackit-service-account-prod.apps.01.cf.eu01.stackit.cloud",
"privateKey": "-----BEGIN PRIVATE KEY-----\nABC\n-----END PRIVATE KEY-----"
}
}

View file

@ -11,6 +11,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/providerserver" "github.com/hashicorp/terraform-plugin-framework/providerserver"
"github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-go/tfprotov6"
"github.com/hashicorp/terraform-plugin-testing/config" "github.com/hashicorp/terraform-plugin-testing/config"
"github.com/hashicorp/terraform-plugin-testing/echoprovider"
"github.com/stackitcloud/terraform-provider-stackit/stackit" "github.com/stackitcloud/terraform-provider-stackit/stackit"
) )
@ -29,6 +30,18 @@ var (
"stackit": providerserver.NewProtocol6WithError(stackit.New("test-version")()), "stackit": providerserver.NewProtocol6WithError(stackit.New("test-version")()),
} }
// TestEphemeralAccProtoV6ProviderFactories is used to instantiate a provider during
// acceptance testing. The factory function will be invoked for every Terraform
// CLI command executed to create a provider server to which the CLI can
// reattach.
//
// See the Terraform acceptance test documentation on ephemeral resources for more information:
// https://developer.hashicorp.com/terraform/plugin/testing/acceptance-tests/ephemeral-resources
TestEphemeralAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){
"stackit": providerserver.NewProtocol6WithError(stackit.New("test-version")()),
"echo": echoprovider.NewProviderServer(),
}
// E2ETestsEnabled checks if end-to-end tests should be run. // E2ETestsEnabled checks if end-to-end tests should be run.
// It is enabled when the TF_ACC environment variable is set to "1". // It is enabled when the TF_ACC environment variable is set to "1".
E2ETestsEnabled = os.Getenv("TF_ACC") == "1" E2ETestsEnabled = os.Getenv("TF_ACC") == "1"

View file

@ -7,6 +7,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
"github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/provider/schema"
@ -18,6 +19,7 @@ import (
"github.com/stackitcloud/stackit-sdk-go/core/config" "github.com/stackitcloud/stackit-sdk-go/core/config"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/access_token"
roleAssignements "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/authorization/roleassignments" roleAssignements "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/authorization/roleassignments"
cdnCustomDomain "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/customdomain" cdnCustomDomain "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/customdomain"
cdn "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/distribution" cdn "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/distribution"
@ -97,7 +99,8 @@ import (
// Ensure the implementation satisfies the expected interfaces // Ensure the implementation satisfies the expected interfaces
var ( var (
_ provider.Provider = &Provider{} _ provider.Provider = &Provider{}
_ provider.ProviderWithEphemeralResources = &Provider{}
) )
// Provider is the provider implementation. // Provider is the provider implementation.
@ -419,7 +422,6 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest,
setStringField(providerConfig.Token, func(v string) { sdkConfig.Token = v }) setStringField(providerConfig.Token, func(v string) { sdkConfig.Token = v })
setStringField(providerConfig.TokenCustomEndpoint, func(v string) { sdkConfig.TokenCustomUrl = v }) setStringField(providerConfig.TokenCustomEndpoint, func(v string) { sdkConfig.TokenCustomUrl = v })
// Provider Data Configuration
setStringField(providerConfig.DefaultRegion, func(v string) { providerData.DefaultRegion = v }) setStringField(providerConfig.DefaultRegion, func(v string) { providerData.DefaultRegion = v })
setStringField(providerConfig.Region, func(v string) { providerData.Region = v }) // nolint:staticcheck // preliminary handling of deprecated attribute setStringField(providerConfig.Region, func(v string) { providerData.Region = v }) // nolint:staticcheck // preliminary handling of deprecated attribute
setBoolField(providerConfig.EnableBetaResources, func(v bool) { providerData.EnableBetaResources = v }) setBoolField(providerConfig.EnableBetaResources, func(v bool) { providerData.EnableBetaResources = v })
@ -472,6 +474,16 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest,
resp.DataSourceData = providerData resp.DataSourceData = providerData
resp.ResourceData = providerData resp.ResourceData = providerData
// Copy service account, private key credentials and custom-token endpoint to support ephemeral access token generation
var ephemeralProviderData core.EphemeralProviderData
ephemeralProviderData.ProviderData = providerData
setStringField(providerConfig.ServiceAccountKey, func(v string) { ephemeralProviderData.ServiceAccountKey = v })
setStringField(providerConfig.ServiceAccountKeyPath, func(v string) { ephemeralProviderData.ServiceAccountKeyPath = v })
setStringField(providerConfig.PrivateKey, func(v string) { ephemeralProviderData.PrivateKey = v })
setStringField(providerConfig.PrivateKeyPath, func(v string) { ephemeralProviderData.PrivateKeyPath = v })
setStringField(providerConfig.TokenCustomEndpoint, func(v string) { ephemeralProviderData.TokenCustomEndpoint = v })
resp.EphemeralResourceData = ephemeralProviderData
providerData.Version = p.version providerData.Version = p.version
} }
@ -622,3 +634,10 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource {
return resources return resources
} }
// EphemeralResources defines the ephemeral resources implemented in the provider.
func (p *Provider) EphemeralResources(_ context.Context) []func() ephemeral.EphemeralResource {
return []func() ephemeral.EphemeralResource{
access_token.NewAccessTokenEphemeralResource,
}
}