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:
parent
368b8d55be
commit
0e9b97a513
12 changed files with 733 additions and 5 deletions
|
|
@ -178,8 +178,22 @@ func ParseProviderData(ctx context.Context, providerData any, diags *diag.Diagno
|
|||
|
||||
stackitProviderData, ok := providerData.(core.ProviderData)
|
||||
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 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,8 +16,9 @@ import (
|
|||
type ResourceType string
|
||||
|
||||
const (
|
||||
Resource ResourceType = "resource"
|
||||
Datasource ResourceType = "datasource"
|
||||
Resource ResourceType = "resource"
|
||||
Datasource ResourceType = "datasource"
|
||||
EphemeralResource ResourceType = "ephemeral-resource"
|
||||
|
||||
// Separator used for concatenation of TF-internal resource ID
|
||||
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."
|
||||
)
|
||||
|
||||
type EphemeralProviderData struct {
|
||||
ProviderData
|
||||
|
||||
PrivateKey string
|
||||
PrivateKeyPath string
|
||||
ServiceAccountKey string
|
||||
ServiceAccountKeyPath string
|
||||
TokenCustomEndpoint string
|
||||
}
|
||||
|
||||
type ProviderData struct {
|
||||
RoundTripper http.RoundTripper
|
||||
ServiceAccountEmail string // Deprecated: ServiceAccountEmail is not required and will be removed after 12th June 2025.
|
||||
|
|
|
|||
|
|
@ -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`)),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
132
stackit/internal/services/access_token/ephemeral_resource.go
Normal file
132
stackit/internal/services/access_token/ephemeral_resource.go
Normal 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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
15
stackit/internal/services/access_token/testdata/ephemeral_resource.tf
vendored
Normal file
15
stackit/internal/services/access_token/testdata/ephemeral_resource.tf
vendored
Normal 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" {
|
||||
}
|
||||
16
stackit/internal/services/access_token/testdata/service_account.json
vendored
Normal file
16
stackit/internal/services/access_token/testdata/service_account.json
vendored
Normal 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-----"
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/hashicorp/terraform-plugin-framework/providerserver"
|
||||
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
|
||||
"github.com/hashicorp/terraform-plugin-testing/config"
|
||||
"github.com/hashicorp/terraform-plugin-testing/echoprovider"
|
||||
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit"
|
||||
)
|
||||
|
|
@ -29,6 +30,18 @@ var (
|
|||
"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.
|
||||
// It is enabled when the TF_ACC environment variable is set to "1".
|
||||
E2ETestsEnabled = os.Getenv("TF_ACC") == "1"
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
|
||||
"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/provider"
|
||||
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
|
||||
|
|
@ -18,6 +19,7 @@ import (
|
|||
"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/features"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/access_token"
|
||||
roleAssignements "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/authorization/roleassignments"
|
||||
cdnCustomDomain "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/customdomain"
|
||||
cdn "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/distribution"
|
||||
|
|
@ -97,7 +99,8 @@ import (
|
|||
|
||||
// Ensure the implementation satisfies the expected interfaces
|
||||
var (
|
||||
_ provider.Provider = &Provider{}
|
||||
_ provider.Provider = &Provider{}
|
||||
_ provider.ProviderWithEphemeralResources = &Provider{}
|
||||
)
|
||||
|
||||
// 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.TokenCustomEndpoint, func(v string) { sdkConfig.TokenCustomUrl = v })
|
||||
|
||||
// Provider Data Configuration
|
||||
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
|
||||
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.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
|
||||
}
|
||||
|
||||
|
|
@ -622,3 +634,10 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource {
|
|||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue