From d95d6b88aa1327ac22657efa74c5745ca2fcd7c2 Mon Sep 17 00:00:00 2001 From: Jorge Turrado Date: Tue, 6 Jan 2026 22:05:53 +0100 Subject: [PATCH 01/22] initial commit Signed-off-by: Jorge Turrado --- go.mod | 2 + go.sum | 4 +- .../internal/conversion/conversion_test.go | 24 +-- stackit/internal/core/core.go | 15 +- .../access_token/ephemeral_resource.go | 35 ++-- .../access_token/ephemeral_resource_test.go | 9 +- stackit/provider.go | 153 +++++++++++++----- 7 files changed, 161 insertions(+), 81 deletions(-) diff --git a/go.mod b/go.mod index 4be8fd6f9..60b3c097b 100644 --- a/go.mod +++ b/go.mod @@ -43,6 +43,8 @@ require ( golang.org/x/mod v0.31.0 ) +replace github.com/stackitcloud/stackit-sdk-go/core => github.com/JorTurFer/stackit-sdk-go/core v0.0.0-20251223142803-960a8fc17ae9 + require ( github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/kr/text v0.2.0 // indirect diff --git a/go.sum b/go.sum index c872fd30a..71e4b8c8a 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/JorTurFer/stackit-sdk-go/core v0.0.0-20251223142803-960a8fc17ae9 h1:cbSzCqmQ6SAB5vQrSMN1Zaf1DeGYh5heX3liol8XO2Q= +github.com/JorTurFer/stackit-sdk-go/core v0.0.0-20251223142803-960a8fc17ae9/go.mod h1:fqto7M82ynGhEnpZU6VkQKYWYoFG5goC076JWXTUPRQ= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= @@ -149,8 +151,6 @@ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= -github.com/stackitcloud/stackit-sdk-go/core v0.20.1 h1:odiuhhRXmxvEvnVTeZSN9u98edvw2Cd3DcnkepncP3M= -github.com/stackitcloud/stackit-sdk-go/core v0.20.1/go.mod h1:fqto7M82ynGhEnpZU6VkQKYWYoFG5goC076JWXTUPRQ= github.com/stackitcloud/stackit-sdk-go/services/authorization v0.9.0 h1:7ZKd3b+E/R4TEVShLTXxx5FrsuDuJBOyuVOuKTMa4mo= github.com/stackitcloud/stackit-sdk-go/services/authorization v0.9.0/go.mod h1:/FoXa6hF77Gv8brrvLBCKa5ie1Xy9xn39yfHwaln9Tw= github.com/stackitcloud/stackit-sdk-go/services/cdn v1.6.0 h1:Q+qIdejeMsYMkbtVoI9BpGlKGdSVFRBhH/zj44SP8TM= diff --git a/stackit/internal/conversion/conversion_test.go b/stackit/internal/conversion/conversion_test.go index 08083abb2..4a5593b0f 100644 --- a/stackit/internal/conversion/conversion_test.go +++ b/stackit/internal/conversion/conversion_test.go @@ -354,21 +354,25 @@ func TestParseEphemeralProviderData(t *testing.T) { 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: "", + PrivateKey: "", + PrivateKeyPath: "/home/dev/foo/private-key.json", + ServiceAccountKey: "", + ServiceAccountKeyPath: "/home/dev/foo/key.json", + TokenCustomEndpoint: "", + ServiceAccountFederatedTokenPath: "/home/dev/foo/token", + ServiceAccountEmail: "test@sa.stackit.cloud", }, }, want: want{ ok: true, providerData: core.EphemeralProviderData{ - PrivateKey: "", - PrivateKeyPath: "/home/dev/foo/private-key.json", - ServiceAccountKey: "", - ServiceAccountKeyPath: "/home/dev/foo/key.json", - TokenCustomEndpoint: "", + PrivateKey: "", + PrivateKeyPath: "/home/dev/foo/private-key.json", + ServiceAccountKey: "", + ServiceAccountKeyPath: "/home/dev/foo/key.json", + TokenCustomEndpoint: "", + ServiceAccountFederatedTokenPath: "/home/dev/foo/token", + ServiceAccountEmail: "test@sa.stackit.cloud", }, }, wantErr: false, diff --git a/stackit/internal/core/core.go b/stackit/internal/core/core.go index 688335ec0..1de47f6dd 100644 --- a/stackit/internal/core/core.go +++ b/stackit/internal/core/core.go @@ -30,16 +30,19 @@ const ( type EphemeralProviderData struct { ProviderData - PrivateKey string - PrivateKeyPath string - ServiceAccountKey string - ServiceAccountKeyPath string - TokenCustomEndpoint string + PrivateKey string + PrivateKeyPath string + ServiceAccountKey string + ServiceAccountKeyPath string + ServiceAccountFederatedTokenPath string + ServiceAccountFederatedToken string + ServiceAccountEmail string + TokenCustomEndpoint string } type ProviderData struct { RoundTripper http.RoundTripper - ServiceAccountEmail string // Deprecated: ServiceAccountEmail is not required and will be removed after 12th June 2025. + ServiceAccountEmail string // Deprecated: Use DefaultRegion instead Region string DefaultRegion string diff --git a/stackit/internal/services/access_token/ephemeral_resource.go b/stackit/internal/services/access_token/ephemeral_resource.go index 8ae346ba3..14b4c9bf7 100644 --- a/stackit/internal/services/access_token/ephemeral_resource.go +++ b/stackit/internal/services/access_token/ephemeral_resource.go @@ -25,7 +25,7 @@ func NewAccessTokenEphemeralResource() ephemeral.EphemeralResource { } type accessTokenEphemeralResource struct { - keyAuthConfig config.Configuration + authConfig config.Configuration } func (e *accessTokenEphemeralResource) Configure(ctx context.Context, req ephemeral.ConfigureRequest, resp *ephemeral.ConfigureResponse) { @@ -44,12 +44,15 @@ func (e *accessTokenEphemeralResource) Configure(ctx context.Context, req epheme return } - e.keyAuthConfig = config.Configuration{ - ServiceAccountKey: ephemeralProviderData.ServiceAccountKey, - ServiceAccountKeyPath: ephemeralProviderData.ServiceAccountKeyPath, - PrivateKeyPath: ephemeralProviderData.PrivateKey, - PrivateKey: ephemeralProviderData.PrivateKeyPath, - TokenCustomUrl: ephemeralProviderData.TokenCustomEndpoint, + e.authConfig = config.Configuration{ + ServiceAccountKey: ephemeralProviderData.ServiceAccountKey, + ServiceAccountKeyPath: ephemeralProviderData.ServiceAccountKeyPath, + PrivateKeyPath: ephemeralProviderData.PrivateKey, + PrivateKey: ephemeralProviderData.PrivateKeyPath, + TokenCustomUrl: ephemeralProviderData.TokenCustomEndpoint, + ServiceAccountFederatedTokenPath: ephemeralProviderData.ServiceAccountFederatedTokenPath, + ServiceAccountFederatedToken: ephemeralProviderData.ServiceAccountFederatedToken, + ServiceAccountEmail: ephemeralProviderData.ServiceAccountEmail, } } @@ -95,7 +98,7 @@ func (e *accessTokenEphemeralResource) Open(ctx context.Context, req ephemeral.O return } - accessToken, err := getAccessToken(&e.keyAuthConfig) + accessToken, err := getAccessToken(&e.authConfig) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Access token generation failed", err.Error()) return @@ -107,7 +110,7 @@ func (e *accessTokenEphemeralResource) Open(ctx context.Context, req ephemeral.O // 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) + roundTripper, err := auth.SetupAuth(keyAuthConfig) if err != nil { return "", fmt.Errorf( "failed to initialize authentication: %w. "+ @@ -117,13 +120,15 @@ func getAccessToken(keyAuthConfig *config.Configuration) (string, error) { } // 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") + var accessToken string + switch client := roundTripper.(type) { + case *clients.KeyFlow: + accessToken, err = client.GetAccessToken() + case *clients.WorkloadIdentityFederationFlow: + accessToken, err = client.GetAccessToken() + default: + return "", fmt.Errorf("internal error: expected KeyFlow or WorkloadIdentityFlow, 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) } diff --git a/stackit/internal/services/access_token/ephemeral_resource_test.go b/stackit/internal/services/access_token/ephemeral_resource_test.go index 5df2b91ce..c9d1a7531 100644 --- a/stackit/internal/services/access_token/ephemeral_resource_test.go +++ b/stackit/internal/services/access_token/ephemeral_resource_test.go @@ -23,11 +23,10 @@ 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", + AccessToken: "mock_access_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) diff --git a/stackit/provider.go b/stackit/provider.go index d93b2b3e1..35c1fec55 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -129,12 +129,15 @@ func (p *Provider) Metadata(_ context.Context, _ provider.MetadataRequest, resp type providerModel struct { CredentialsFilePath types.String `tfsdk:"credentials_path"` - ServiceAccountEmail types.String `tfsdk:"service_account_email"` // Deprecated: ServiceAccountEmail is not required and will be removed after 12th June 2025 + ServiceAccountEmail types.String `tfsdk:"service_account_email"` ServiceAccountKey types.String `tfsdk:"service_account_key"` ServiceAccountKeyPath types.String `tfsdk:"service_account_key_path"` PrivateKey types.String `tfsdk:"private_key"` PrivateKeyPath types.String `tfsdk:"private_key_path"` Token types.String `tfsdk:"service_account_token"` + WifFederatedTokenPath types.String `tfsdk:"service_account_federated_token_path"` + UseOIDC types.Bool `tfsdk:"use_oidc"` + // Deprecated: Use DefaultRegion instead Region types.String `tfsdk:"region"` DefaultRegion types.String `tfsdk:"default_region"` @@ -176,45 +179,47 @@ type providerModel struct { // Schema defines the provider-level schema for configuration data. func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) { descriptions := map[string]string{ - "credentials_path": "Path of JSON from where the credentials are read. Takes precedence over the env var `STACKIT_CREDENTIALS_PATH`. Default value is `~/.stackit/credentials.json`.", - "service_account_token": "Token used for authentication. If set, the token flow will be used to authenticate all operations.", - "service_account_key_path": "Path for the service account key used for authentication. If set, the key flow will be used to authenticate all operations.", - "service_account_key": "Service account key used for authentication. If set, the key flow will be used to authenticate all operations.", - "private_key_path": "Path for the private RSA key used for authentication, relevant for the key flow. It takes precedence over the private key that is included in the service account key.", - "private_key": "Private RSA key used for authentication, relevant for the key flow. It takes precedence over the private key that is included in the service account key.", - "service_account_email": "Service account email. It can also be set using the environment variable STACKIT_SERVICE_ACCOUNT_EMAIL. It is required if you want to use the resource manager project resource.", - "region": "Region will be used as the default location for regional services. Not all services require a region, some are global", - "default_region": "Region will be used as the default location for regional services. Not all services require a region, some are global", - "cdn_custom_endpoint": "Custom endpoint for the CDN service", - "dns_custom_endpoint": "Custom endpoint for the DNS service", - "git_custom_endpoint": "Custom endpoint for the Git service", - "iaas_custom_endpoint": "Custom endpoint for the IaaS service", - "kms_custom_endpoint": "Custom endpoint for the KMS service", - "mongodbflex_custom_endpoint": "Custom endpoint for the MongoDB Flex service", - "modelserving_custom_endpoint": "Custom endpoint for the AI Model Serving service", - "loadbalancer_custom_endpoint": "Custom endpoint for the Load Balancer service", - "logme_custom_endpoint": "Custom endpoint for the LogMe service", - "rabbitmq_custom_endpoint": "Custom endpoint for the RabbitMQ service", - "mariadb_custom_endpoint": "Custom endpoint for the MariaDB service", - "authorization_custom_endpoint": "Custom endpoint for the Membership service", - "objectstorage_custom_endpoint": "Custom endpoint for the Object Storage service", - "observability_custom_endpoint": "Custom endpoint for the Observability service", - "opensearch_custom_endpoint": "Custom endpoint for the OpenSearch service", - "postgresflex_custom_endpoint": "Custom endpoint for the PostgresFlex service", - "redis_custom_endpoint": "Custom endpoint for the Redis service", - "server_backup_custom_endpoint": "Custom endpoint for the Server Backup service", - "server_update_custom_endpoint": "Custom endpoint for the Server Update service", - "service_account_custom_endpoint": "Custom endpoint for the Service Account service", - "resourcemanager_custom_endpoint": "Custom endpoint for the Resource Manager service", - "scf_custom_endpoint": "Custom endpoint for the Cloud Foundry (SCF) service", - "secretsmanager_custom_endpoint": "Custom endpoint for the Secrets Manager service", - "sqlserverflex_custom_endpoint": "Custom endpoint for the SQL Server Flex service", - "ske_custom_endpoint": "Custom endpoint for the Kubernetes Engine (SKE) service", - "service_enablement_custom_endpoint": "Custom endpoint for the Service Enablement API", - "sfs_custom_endpoint": "Custom endpoint for the Stackit Filestorage API", - "token_custom_endpoint": "Custom endpoint for the token API, which is used to request access tokens when using the key flow", - "enable_beta_resources": "Enable beta resources. Default is false.", - "experiments": fmt.Sprintf("Enables experiments. These are unstable features without official support. More information can be found in the README. Available Experiments: %v", strings.Join(features.AvailableExperiments, ", ")), + "credentials_path": "Path of JSON from where the credentials are read. Takes precedence over the env var `STACKIT_CREDENTIALS_PATH`. Default value is `~/.stackit/credentials.json`.", + "service_account_token": "Token used for authentication. If set, the token flow will be used to authenticate all operations.", + "service_account_key_path": "Path for the service account key used for authentication. If set, the key flow will be used to authenticate all operations.", + "service_account_key": "Service account key used for authentication. If set, the key flow will be used to authenticate all operations.", + "private_key_path": "Path for the private RSA key used for authentication, relevant for the key flow. It takes precedence over the private key that is included in the service account key.", + "private_key": "Private RSA key used for authentication, relevant for the key flow. It takes precedence over the private key that is included in the service account key.", + "service_account_email": "Service account email. It can also be set using the environment variable STACKIT_SERVICE_ACCOUNT_EMAIL. It is required if you want to use the resource manager project resource.", + "service_account_federated_token_path": "Path for workload identity assertion. It can also be set using the environment variable STACKIT_FEDERATED_TOKEN_FILE.", + "use_oidc": "Should OIDC be used for Authentication? This can also be sourced from the `STACKIT_USE_OIDC` Environment Variable. Defaults to `false`.", + "region": "Region will be used as the default location for regional services. Not all services require a region, some are global", + "default_region": "Region will be used as the default location for regional services. Not all services require a region, some are global", + "cdn_custom_endpoint": "Custom endpoint for the CDN service", + "dns_custom_endpoint": "Custom endpoint for the DNS service", + "git_custom_endpoint": "Custom endpoint for the Git service", + "iaas_custom_endpoint": "Custom endpoint for the IaaS service", + "kms_custom_endpoint": "Custom endpoint for the KMS service", + "mongodbflex_custom_endpoint": "Custom endpoint for the MongoDB Flex service", + "modelserving_custom_endpoint": "Custom endpoint for the AI Model Serving service", + "loadbalancer_custom_endpoint": "Custom endpoint for the Load Balancer service", + "logme_custom_endpoint": "Custom endpoint for the LogMe service", + "rabbitmq_custom_endpoint": "Custom endpoint for the RabbitMQ service", + "mariadb_custom_endpoint": "Custom endpoint for the MariaDB service", + "authorization_custom_endpoint": "Custom endpoint for the Membership service", + "objectstorage_custom_endpoint": "Custom endpoint for the Object Storage service", + "observability_custom_endpoint": "Custom endpoint for the Observability service", + "opensearch_custom_endpoint": "Custom endpoint for the OpenSearch service", + "postgresflex_custom_endpoint": "Custom endpoint for the PostgresFlex service", + "redis_custom_endpoint": "Custom endpoint for the Redis service", + "server_backup_custom_endpoint": "Custom endpoint for the Server Backup service", + "server_update_custom_endpoint": "Custom endpoint for the Server Update service", + "service_account_custom_endpoint": "Custom endpoint for the Service Account service", + "resourcemanager_custom_endpoint": "Custom endpoint for the Resource Manager service", + "scf_custom_endpoint": "Custom endpoint for the Cloud Foundry (SCF) service", + "secretsmanager_custom_endpoint": "Custom endpoint for the Secrets Manager service", + "sqlserverflex_custom_endpoint": "Custom endpoint for the SQL Server Flex service", + "ske_custom_endpoint": "Custom endpoint for the Kubernetes Engine (SKE) service", + "service_enablement_custom_endpoint": "Custom endpoint for the Service Enablement API", + "sfs_custom_endpoint": "Custom endpoint for the Stackit Filestorage API", + "token_custom_endpoint": "Custom endpoint for the token API, which is used to request access tokens when using the key flow", + "enable_beta_resources": "Enable beta resources. Default is false.", + "experiments": fmt.Sprintf("Enables experiments. These are unstable features without official support. More information can be found in the README. Available Experiments: %v", strings.Join(features.AvailableExperiments, ", ")), } resp.Schema = schema.Schema{ @@ -224,9 +229,8 @@ func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *pro Description: descriptions["credentials_path"], }, "service_account_email": schema.StringAttribute{ - Optional: true, - Description: descriptions["service_account_email"], - DeprecationMessage: "The `service_account_email` field has been deprecated because it is not required. Will be removed after June 12th 2025.", + Optional: true, + Description: descriptions["service_account_email"], }, "service_account_token": schema.StringAttribute{ Optional: true, @@ -251,6 +255,14 @@ func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *pro Optional: true, Description: descriptions["private_key_path"], }, + "service_account_federated_token_path": schema.StringAttribute{ + Optional: true, + Description: descriptions["service_account_federated_token_path"], + }, + "use_oidc": schema.BoolAttribute{ + Optional: true, + Description: descriptions["use_oidc"], + }, "region": schema.StringAttribute{ Optional: true, Description: descriptions["region"], @@ -426,10 +438,13 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, // Configure SDK client setStringField(providerConfig.CredentialsFilePath, func(v string) { sdkConfig.CredentialsFilePath = v }) + setStringField(providerConfig.ServiceAccountEmail, func(v string) { sdkConfig.ServiceAccountEmail = v }) setStringField(providerConfig.ServiceAccountKey, func(v string) { sdkConfig.ServiceAccountKey = v }) setStringField(providerConfig.ServiceAccountKeyPath, func(v string) { sdkConfig.ServiceAccountKeyPath = v }) setStringField(providerConfig.PrivateKey, func(v string) { sdkConfig.PrivateKey = v }) setStringField(providerConfig.PrivateKeyPath, func(v string) { sdkConfig.PrivateKeyPath = v }) + setStringField(providerConfig.WifFederatedTokenPath, func(v string) { sdkConfig.ServiceAccountFederatedTokenPath = v }) + setBoolField(providerConfig.UseOIDC, func(v bool) { sdkConfig.WorkloadIdentityFederation = v }) setStringField(providerConfig.Token, func(v string) { sdkConfig.Token = v }) setStringField(providerConfig.TokenCustomEndpoint, func(v string) { sdkConfig.TokenCustomUrl = v }) @@ -474,6 +489,10 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, providerData.Experiments = experimentValues } + if sdkConfig.WorkloadIdentityFederation { + // + } + roundTripper, err := sdkauth.SetupAuth(sdkConfig) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring provider", fmt.Sprintf("Setting up authentication: %v", err)) @@ -489,11 +508,13 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, // 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.ServiceAccountEmail, func(v string) { ephemeralProviderData.ServiceAccountEmail = v }) 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 }) + setStringField(providerConfig.WifFederatedTokenPath, func(v string) { ephemeralProviderData.ServiceAccountFederatedTokenPath = v }) resp.EphemeralResourceData = ephemeralProviderData providerData.Version = p.version @@ -662,3 +683,49 @@ func (p *Provider) EphemeralResources(_ context.Context) []func() ephemeral.Ephe access_token.NewAccessTokenEphemeralResource, } } + +// func (a *GitHubOIDCAuthorizer) githubAssertion(ctx context.Context, _ *http.Request) (*string, error) { +// req, err := http.NewRequestWithContext(ctx, http.MethodGet, a.conf.IDTokenRequestURL, http.NoBody) +// if err != nil { +// return nil, fmt.Errorf("githubAssertion: failed to build request: %+v", err) +// } + +// query, err := url.ParseQuery(req.URL.RawQuery) +// if err != nil { +// return nil, fmt.Errorf("githubAssertion: cannot parse URL query") +// } + +// if query.Get("audience") == "" { +// query.Set("audience", "api://AzureADTokenExchange") +// req.URL.RawQuery = query.Encode() +// } + +// req.Header.Set("Accept", "application/json") +// req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", a.conf.IDTokenRequestToken)) +// req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + +// resp, err := Client.Do(req) +// if err != nil { +// return nil, fmt.Errorf("githubAssertion: cannot request token: %v", err) +// } + +// defer resp.Body.Close() +// body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) +// if err != nil { +// return nil, fmt.Errorf("githubAssertion: cannot parse response: %v", err) +// } + +// if c := resp.StatusCode; c < 200 || c > 299 { +// return nil, fmt.Errorf("githubAssertion: received HTTP status %d with response: %s", resp.StatusCode, body) +// } + +// var tokenRes struct { +// Count *int `json:"count"` +// Value *string `json:"value"` +// } +// if err := json.Unmarshal(body, &tokenRes); err != nil { +// return nil, fmt.Errorf("githubAssertion: cannot unmarshal response: %v", err) +// } + +// return tokenRes.Value, nil +// } From 6566b5750e2fb9257399eaa4db91935587d7204d Mon Sep 17 00:00:00 2001 From: Jorge Turrado Date: Tue, 6 Jan 2026 23:02:45 +0100 Subject: [PATCH 02/22] request id token for GH Signed-off-by: Jorge Turrado --- stackit/provider.go | 121 +++++++++++++++++++++++++++----------------- 1 file changed, 75 insertions(+), 46 deletions(-) diff --git a/stackit/provider.go b/stackit/provider.go index 35c1fec55..99479a44d 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -2,7 +2,11 @@ package stackit import ( "context" + "encoding/json" "fmt" + "io" + "net/http" + "os" "strings" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" @@ -171,6 +175,8 @@ type providerModel struct { SkeCustomEndpoint types.String `tfsdk:"ske_custom_endpoint"` SqlServerFlexCustomEndpoint types.String `tfsdk:"sqlserverflex_custom_endpoint"` TokenCustomEndpoint types.String `tfsdk:"token_custom_endpoint"` + OIDCTokenRequestURL types.String `tfsdk:"oidc_request_url"` + OIDCTokenRequestToken types.String `tfsdk:"oidc_request_token"` EnableBetaResources types.Bool `tfsdk:"enable_beta_resources"` Experiments types.List `tfsdk:"experiments"` @@ -188,6 +194,8 @@ func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *pro "service_account_email": "Service account email. It can also be set using the environment variable STACKIT_SERVICE_ACCOUNT_EMAIL. It is required if you want to use the resource manager project resource.", "service_account_federated_token_path": "Path for workload identity assertion. It can also be set using the environment variable STACKIT_FEDERATED_TOKEN_FILE.", "use_oidc": "Should OIDC be used for Authentication? This can also be sourced from the `STACKIT_USE_OIDC` Environment Variable. Defaults to `false`.", + "oidc_request_url": "The URL for the OIDC provider from which to request an ID token. For use when authenticating as a Service Account using OpenID Connect.", + "oidc_request_token": "The bearer token for the request to the OIDC provider. For use when authenticating as a Service Account using OpenID Connect.", "region": "Region will be used as the default location for regional services. Not all services require a region, some are global", "default_region": "Region will be used as the default location for regional services. Not all services require a region, some are global", "cdn_custom_endpoint": "Custom endpoint for the CDN service", @@ -263,6 +271,14 @@ func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *pro Optional: true, Description: descriptions["use_oidc"], }, + "oidc_request_token": schema.StringAttribute{ + Optional: true, + Description: descriptions["oidc_request_token"], + }, + "oidc_request_url": schema.StringAttribute{ + Optional: true, + Description: descriptions["oidc_request_url"], + }, "region": schema.StringAttribute{ Optional: true, Description: descriptions["region"], @@ -490,7 +506,17 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, } if sdkConfig.WorkloadIdentityFederation { - // + // https://docs.github.com/en/actions/reference/security/oidc#methods-for-requesting-the-oidc-token + oidcReqURL := getEnvStringOrDefault(providerConfig.OIDCTokenRequestURL, "ACTIONS_ID_TOKEN_REQUEST_URL", "") + oidcReqToken := getEnvStringOrDefault(providerConfig.OIDCTokenRequestToken, "ACTIONS_ID_TOKEN_REQUEST_TOKEN", "") + if oidcReqURL != "" && oidcReqToken != "" { + id_token, err := githubAssertion(ctx, oidcReqURL, oidcReqToken) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring provider", fmt.Sprintf("Requesting id token from Github %v", err)) + return + } + sdkConfig.ServiceAccountFederatedToken = id_token + } } roundTripper, err := sdkauth.SetupAuth(sdkConfig) @@ -684,48 +710,51 @@ func (p *Provider) EphemeralResources(_ context.Context) []func() ephemeral.Ephe } } -// func (a *GitHubOIDCAuthorizer) githubAssertion(ctx context.Context, _ *http.Request) (*string, error) { -// req, err := http.NewRequestWithContext(ctx, http.MethodGet, a.conf.IDTokenRequestURL, http.NoBody) -// if err != nil { -// return nil, fmt.Errorf("githubAssertion: failed to build request: %+v", err) -// } - -// query, err := url.ParseQuery(req.URL.RawQuery) -// if err != nil { -// return nil, fmt.Errorf("githubAssertion: cannot parse URL query") -// } - -// if query.Get("audience") == "" { -// query.Set("audience", "api://AzureADTokenExchange") -// req.URL.RawQuery = query.Encode() -// } - -// req.Header.Set("Accept", "application/json") -// req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", a.conf.IDTokenRequestToken)) -// req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - -// resp, err := Client.Do(req) -// if err != nil { -// return nil, fmt.Errorf("githubAssertion: cannot request token: %v", err) -// } - -// defer resp.Body.Close() -// body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) -// if err != nil { -// return nil, fmt.Errorf("githubAssertion: cannot parse response: %v", err) -// } - -// if c := resp.StatusCode; c < 200 || c > 299 { -// return nil, fmt.Errorf("githubAssertion: received HTTP status %d with response: %s", resp.StatusCode, body) -// } - -// var tokenRes struct { -// Count *int `json:"count"` -// Value *string `json:"value"` -// } -// if err := json.Unmarshal(body, &tokenRes); err != nil { -// return nil, fmt.Errorf("githubAssertion: cannot unmarshal response: %v", err) -// } - -// return tokenRes.Value, nil -// } +func githubAssertion(ctx context.Context, oidc_request_url, oidc_request_token string) (string, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, oidc_request_url, http.NoBody) + if err != nil { + return "", fmt.Errorf("githubAssertion: failed to build request: %+v", err) + } + + req.Header.Set("Accept", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", oidc_request_token)) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", fmt.Errorf("githubAssertion: cannot request token: %v", err) + } + + defer resp.Body.Close() + body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if err != nil { + return "", fmt.Errorf("githubAssertion: cannot parse response: %v", err) + } + + if c := resp.StatusCode; c < 200 || c > 299 { + return "", fmt.Errorf("githubAssertion: received HTTP status %d with response: %s", resp.StatusCode, body) + } + + var tokenRes struct { + Value string `json:"value"` + } + if err := json.Unmarshal(body, &tokenRes); err != nil { + return "", fmt.Errorf("githubAssertion: cannot unmarshal response: %v", err) + } + + return tokenRes.Value, nil +} + +// getEnvStringOrDefault takes a Framework StringValue and a corresponding Environment Variable name and returns +// either the string value set in the StringValue if not Null / Unknown _or_ the os.GetEnv() value of the Environment +// Variable provided. If both of these are empty, an empty string defaultValue is returned. +func getEnvStringOrDefault(val types.String, envVar string, defaultValue string) string { + if val.IsNull() || val.IsUnknown() { + if v := os.Getenv(envVar); v != "" { + return os.Getenv(envVar) + } + return defaultValue + } + + return val.ValueString() +} From 4dc71ae2c24e27e3c0af3b3fe9b2d21a4f2a1bce Mon Sep 17 00:00:00 2001 From: Jorge Turrado Date: Wed, 7 Jan 2026 00:06:30 +0100 Subject: [PATCH 03/22] . Signed-off-by: Jorge Turrado --- .github/workflows/tmp.yaml | 99 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 .github/workflows/tmp.yaml diff --git a/.github/workflows/tmp.yaml b/.github/workflows/tmp.yaml new file mode 100644 index 000000000..c7c90e1da --- /dev/null +++ b/.github/workflows/tmp.yaml @@ -0,0 +1,99 @@ +name: Build and Validate Provider + +on: + push: + branches: + - '**' # Ejecutar en cualquier rama + +jobs: + build-and-test: + name: Build Provider & Test Plan + runs-on: ubuntu-latest + + # Configura aquí las variables de entorno necesarias para la autenticación + # Si usas OIDC, asegúrate de que GitHub Actions tenga los permisos o secretos necesarios + env: + # Ejemplo de variables que tu provider podría necesitar para autenticarse: + # STACKIT_SERVICE_ACCOUNT_TOKEN: ${{ secrets.STACKIT_TOKEN }} + # STACKIT_SERVICE_ACCOUNT_KEY: ${{ secrets.STACKIT_KEY }} + GOPROXY: "https://proxy.golang.org,direct" + + steps: + # 1. Descargar el código del repositorio + - name: Checkout Code + uses: actions/checkout@v4 + + # 2. Configurar Go (ajusta la versión según tu go.mod) + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + + # 3. Compilar el Provider + # Es crucial que el binario tenga un nombre reconocible, aunque dev_overrides apunta al directorio. + - name: Build Provider + run: | + go build -o terraform-provider-stackit + chmod +x terraform-provider-stackit + echo "BINARY_PATH=${GITHUB_WORKSPACE}" >> $GITHUB_ENV + + # 4. Configurar Terraform CLI para usar el binario local (Dev Overrides) + # Esto crea el archivo ~/.terraformrc que le dice a TF dónde buscar tu provider + - name: Configure Terraform Dev Overrides + run: | + cat < ~/.terraformrc + provider_installation { + dev_overrides { + "stackitcloud/stackit" = "${{ env.BINARY_PATH }}" + } + # Para otros providers (random, null, etc), usa el registro normal + direct {} + } + EOF + + echo "Terraform RC content:" + cat ~/.terraformrc + + # 5. Instalar Terraform + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_wrapper: false # Recomendado cuando se manipula la salida o config compleja + + # 6. Crear el archivo main.tf con tu código de prueba + - name: Create Test Configuration + run: | + cat < main.tf + terraform { + required_providers { + stackit = { + source = "stackitcloud/stackit" + } + } + } + + provider "stackit" { + default_region = "eu01" + enable_beta_resources = true + service_account_custom_endpoint = "https://service-account.api.qa.stackit.cloud" + token_custom_endpoint = "https://accounts.qa.stackit.cloud/oauth/v2/token" + service_account_email = "test-idp-njf5pc1@sa.stackit.cloud" + use_oidc = true + } + + resource "stackit_service_account" "sa" { + project_id = "62675838-7758-4b99-a4eb-84b20ac8626b" + name = "terraform-wif-2" + } + EOF + + # 7. Ejecutar Terraform Plan + # Nota: init fallará al intentar verificar checksums de dev_overrides, eso es normal, + # por eso usamos -backend=false o simplemente corremos plan directo si no requerimos estado remoto. + - name: Terraform Plan + run: | + # Inicializamos sin backend para pruebas rápidas + terraform init -backend=false + + # Ejecutamos plan. Si el provider carga y la API responde, esto pasará. + terraform plan -out=tfplan \ No newline at end of file From b5ebf729c35a4599032c670040914b68251f15a9 Mon Sep 17 00:00:00 2001 From: Jorge Turrado Date: Wed, 7 Jan 2026 00:08:15 +0100 Subject: [PATCH 04/22] . Signed-off-by: Jorge Turrado --- .github/workflows/tmp.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tmp.yaml b/.github/workflows/tmp.yaml index c7c90e1da..c9eba9420 100644 --- a/.github/workflows/tmp.yaml +++ b/.github/workflows/tmp.yaml @@ -4,6 +4,7 @@ on: push: branches: - '**' # Ejecutar en cualquier rama + workflow_dispatch: jobs: build-and-test: From 203db08e2fceef4ad3218d76d10adcb58e048de9 Mon Sep 17 00:00:00 2001 From: Jorge Turrado Date: Wed, 7 Jan 2026 00:09:36 +0100 Subject: [PATCH 05/22] . Signed-off-by: Jorge Turrado --- .github/workflows/tmp.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/tmp.yaml b/.github/workflows/tmp.yaml index c9eba9420..f03a8e890 100644 --- a/.github/workflows/tmp.yaml +++ b/.github/workflows/tmp.yaml @@ -10,6 +10,9 @@ jobs: build-and-test: name: Build Provider & Test Plan runs-on: ubuntu-latest + permissions: + contents: read # Necesario para descargar el código (actions/checkout) + id-token: write # OBLIGATORIO para que 'use_oidc = true' funcione # Configura aquí las variables de entorno necesarias para la autenticación # Si usas OIDC, asegúrate de que GitHub Actions tenga los permisos o secretos necesarios From cd88388ba372e18d121fd15c4c38ee0c03e649fb Mon Sep 17 00:00:00 2001 From: Jorge Turrado Date: Wed, 7 Jan 2026 00:14:08 +0100 Subject: [PATCH 06/22] . Signed-off-by: Jorge Turrado --- .github/workflows/tmp.yaml | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tmp.yaml b/.github/workflows/tmp.yaml index f03a8e890..fbc351666 100644 --- a/.github/workflows/tmp.yaml +++ b/.github/workflows/tmp.yaml @@ -87,7 +87,7 @@ jobs: resource "stackit_service_account" "sa" { project_id = "62675838-7758-4b99-a4eb-84b20ac8626b" - name = "terraform-wif-2" + name = "terraform-wif-ci" } EOF @@ -100,4 +100,20 @@ jobs: terraform init -backend=false # Ejecutamos plan. Si el provider carga y la API responde, esto pasará. - terraform plan -out=tfplan \ No newline at end of file + terraform plan -out=tfplan + + - name: Terraform Apply + run: terraform apply -auto-approve tfplan + + # PASO RECOMENDADO: Destruye los recursos al finalizar para no dejar basura + # ni causar errores de "resource already exists" en el siguiente push. + # 'if: always()' asegura que se ejecute aunque los pasos anteriores fallen, + # siempre que se haya creado algo de estado. + - name: Terraform Destroy + if: always() + run: | + if [ -f terraform.tfstate ]; then + terraform destroy -auto-approve + else + echo "No state file found, skipping destroy." + fi \ No newline at end of file From c61a7b3cbc8240292c12999d03f7e3e6947ec3bc Mon Sep 17 00:00:00 2001 From: Jorge Turrado Ferrero Date: Wed, 7 Jan 2026 10:59:46 +0100 Subject: [PATCH 07/22] Add audience parameter to GitHub request query --- stackit/provider.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/stackit/provider.go b/stackit/provider.go index 99479a44d..1f69c0573 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -716,6 +716,16 @@ func githubAssertion(ctx context.Context, oidc_request_url, oidc_request_token s return "", fmt.Errorf("githubAssertion: failed to build request: %+v", err) } + query, err := url.ParseQuery(req.URL.RawQuery) + if err != nil { + return nil, fmt.Errorf("githubAssertion: cannot parse URL query") + } + + if query.Get("audience") == "" { + query.Set("audience", "sts.accounts.stackit.cloud") + req.URL.RawQuery = query.Encode() + } + req.Header.Set("Accept", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", oidc_request_token)) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") From 691c7056c2f9b61e6d8fb8493f37dd892beda2e8 Mon Sep 17 00:00:00 2001 From: Jorge Turrado Ferrero Date: Wed, 7 Jan 2026 11:04:10 +0100 Subject: [PATCH 08/22] Update provider.go --- stackit/provider.go | 1 + 1 file changed, 1 insertion(+) diff --git a/stackit/provider.go b/stackit/provider.go index 1f69c0573..aadcb4c3e 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "net/http" + "net/url" "os" "strings" From 9d8ca10124d97a1d955eb5a3a0c31760129ebde2 Mon Sep 17 00:00:00 2001 From: Jorge Turrado Ferrero Date: Wed, 7 Jan 2026 11:04:53 +0100 Subject: [PATCH 09/22] Fix return type in URL query parsing error --- stackit/provider.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackit/provider.go b/stackit/provider.go index aadcb4c3e..dde58775e 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -719,7 +719,7 @@ func githubAssertion(ctx context.Context, oidc_request_url, oidc_request_token s query, err := url.ParseQuery(req.URL.RawQuery) if err != nil { - return nil, fmt.Errorf("githubAssertion: cannot parse URL query") + return "", fmt.Errorf("githubAssertion: cannot parse URL query") } if query.Get("audience") == "" { From 3d5baa1a57584f39b34aa90844fb2b67df8714e3 Mon Sep 17 00:00:00 2001 From: Jorge Turrado Date: Wed, 7 Jan 2026 13:19:18 +0100 Subject: [PATCH 10/22] support passing the oidc token directly Signed-off-by: Jorge Turrado --- stackit/provider.go | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/stackit/provider.go b/stackit/provider.go index dde58775e..349f52a13 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -141,6 +141,7 @@ type providerModel struct { PrivateKeyPath types.String `tfsdk:"private_key_path"` Token types.String `tfsdk:"service_account_token"` WifFederatedTokenPath types.String `tfsdk:"service_account_federated_token_path"` + WifFederatedToken types.String `tfsdk:"service_account_federated_token"` UseOIDC types.Bool `tfsdk:"use_oidc"` // Deprecated: Use DefaultRegion instead @@ -194,6 +195,7 @@ func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *pro "private_key": "Private RSA key used for authentication, relevant for the key flow. It takes precedence over the private key that is included in the service account key.", "service_account_email": "Service account email. It can also be set using the environment variable STACKIT_SERVICE_ACCOUNT_EMAIL. It is required if you want to use the resource manager project resource.", "service_account_federated_token_path": "Path for workload identity assertion. It can also be set using the environment variable STACKIT_FEDERATED_TOKEN_FILE.", + "service_account_federated_token": "The OIDC ID token for use when authenticating as a Service Account using OpenID Connect.", "use_oidc": "Should OIDC be used for Authentication? This can also be sourced from the `STACKIT_USE_OIDC` Environment Variable. Defaults to `false`.", "oidc_request_url": "The URL for the OIDC provider from which to request an ID token. For use when authenticating as a Service Account using OpenID Connect.", "oidc_request_token": "The bearer token for the request to the OIDC provider. For use when authenticating as a Service Account using OpenID Connect.", @@ -268,6 +270,10 @@ func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *pro Optional: true, Description: descriptions["service_account_federated_token_path"], }, + "service_account_federated_token": schema.StringAttribute{ + Optional: true, + Description: descriptions["service_account_federated_token"], + }, "use_oidc": schema.BoolAttribute{ Optional: true, Description: descriptions["use_oidc"], @@ -461,6 +467,7 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, setStringField(providerConfig.PrivateKey, func(v string) { sdkConfig.PrivateKey = v }) setStringField(providerConfig.PrivateKeyPath, func(v string) { sdkConfig.PrivateKeyPath = v }) setStringField(providerConfig.WifFederatedTokenPath, func(v string) { sdkConfig.ServiceAccountFederatedTokenPath = v }) + setStringField(providerConfig.WifFederatedToken, func(v string) { sdkConfig.ServiceAccountFederatedToken = v }) setBoolField(providerConfig.UseOIDC, func(v bool) { sdkConfig.WorkloadIdentityFederation = v }) setStringField(providerConfig.Token, func(v string) { sdkConfig.Token = v }) setStringField(providerConfig.TokenCustomEndpoint, func(v string) { sdkConfig.TokenCustomUrl = v }) @@ -506,7 +513,8 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, providerData.Experiments = experimentValues } - if sdkConfig.WorkloadIdentityFederation { + if getEnvBoolIfValueAbsent(providerConfig.UseOIDC, "STACKIT_USE_OIDC") && sdkConfig.ServiceAccountFederatedToken == "" { + sdkConfig.WorkloadIdentityFederation = true // https://docs.github.com/en/actions/reference/security/oidc#methods-for-requesting-the-oidc-token oidcReqURL := getEnvStringOrDefault(providerConfig.OIDCTokenRequestURL, "ACTIONS_ID_TOKEN_REQUEST_URL", "") oidcReqToken := getEnvStringOrDefault(providerConfig.OIDCTokenRequestToken, "ACTIONS_ID_TOKEN_REQUEST_TOKEN", "") @@ -769,3 +777,20 @@ func getEnvStringOrDefault(val types.String, envVar string, defaultValue string) return val.ValueString() } + +// getEnvBoolIfValueAbsent takes a Framework BoolValue and a corresponding Environment Variable name and returns +// one of the following in priority order: +// 1 - the Boolean value set in the BoolValue if this is not Null / Unknown. +// 2 - the boolean representation of the os.GetEnv() value of the Environment Variable provided (where anything but +// 'true' or '1' is 'false'). +// 3 - `false` in all other cases. +func getEnvBoolIfValueAbsent(val types.Bool, envVar string) bool { + if val.IsNull() || val.IsUnknown() { + v := os.Getenv(envVar) + if strings.EqualFold(v, "true") || strings.EqualFold(v, "1") || v == "" { + return true + } + } + + return val.ValueBool() +} From cc88b49d167bd9409ac6632367132b314ca20026 Mon Sep 17 00:00:00 2001 From: Jorge Turrado Date: Wed, 7 Jan 2026 13:23:13 +0100 Subject: [PATCH 11/22] . Signed-off-by: Jorge Turrado --- .github/workflows/tmp.yaml | 33 ++++++--------------------------- 1 file changed, 6 insertions(+), 27 deletions(-) diff --git a/.github/workflows/tmp.yaml b/.github/workflows/tmp.yaml index fbc351666..c9cc03130 100644 --- a/.github/workflows/tmp.yaml +++ b/.github/workflows/tmp.yaml @@ -1,9 +1,11 @@ +# Ignore this file, it is a temporary workflow for testing purposes. + name: Build and Validate Provider on: push: branches: - - '**' # Ejecutar en cualquier rama + - '**' workflow_dispatch: jobs: @@ -11,38 +13,24 @@ jobs: name: Build Provider & Test Plan runs-on: ubuntu-latest permissions: - contents: read # Necesario para descargar el código (actions/checkout) - id-token: write # OBLIGATORIO para que 'use_oidc = true' funcione - - # Configura aquí las variables de entorno necesarias para la autenticación - # Si usas OIDC, asegúrate de que GitHub Actions tenga los permisos o secretos necesarios - env: - # Ejemplo de variables que tu provider podría necesitar para autenticarse: - # STACKIT_SERVICE_ACCOUNT_TOKEN: ${{ secrets.STACKIT_TOKEN }} - # STACKIT_SERVICE_ACCOUNT_KEY: ${{ secrets.STACKIT_KEY }} - GOPROXY: "https://proxy.golang.org,direct" + contents: read + id-token: write steps: - # 1. Descargar el código del repositorio - name: Checkout Code uses: actions/checkout@v4 - # 2. Configurar Go (ajusta la versión según tu go.mod) - name: Setup Go uses: actions/setup-go@v5 with: go-version-file: 'go.mod' - # 3. Compilar el Provider - # Es crucial que el binario tenga un nombre reconocible, aunque dev_overrides apunta al directorio. - name: Build Provider run: | go build -o terraform-provider-stackit chmod +x terraform-provider-stackit echo "BINARY_PATH=${GITHUB_WORKSPACE}" >> $GITHUB_ENV - # 4. Configurar Terraform CLI para usar el binario local (Dev Overrides) - # Esto crea el archivo ~/.terraformrc que le dice a TF dónde buscar tu provider - name: Configure Terraform Dev Overrides run: | cat < ~/.terraformrc @@ -58,13 +46,11 @@ jobs: echo "Terraform RC content:" cat ~/.terraformrc - # 5. Instalar Terraform - name: Setup Terraform uses: hashicorp/setup-terraform@v3 with: - terraform_wrapper: false # Recomendado cuando se manipula la salida o config compleja + terraform_wrapper: false - # 6. Crear el archivo main.tf con tu código de prueba - name: Create Test Configuration run: | cat < main.tf @@ -91,9 +77,6 @@ jobs: } EOF - # 7. Ejecutar Terraform Plan - # Nota: init fallará al intentar verificar checksums de dev_overrides, eso es normal, - # por eso usamos -backend=false o simplemente corremos plan directo si no requerimos estado remoto. - name: Terraform Plan run: | # Inicializamos sin backend para pruebas rápidas @@ -105,10 +88,6 @@ jobs: - name: Terraform Apply run: terraform apply -auto-approve tfplan - # PASO RECOMENDADO: Destruye los recursos al finalizar para no dejar basura - # ni causar errores de "resource already exists" en el siguiente push. - # 'if: always()' asegura que se ejecute aunque los pasos anteriores fallen, - # siempre que se haya creado algo de estado. - name: Terraform Destroy if: always() run: | From e1204c49a6e65644d553b52e15cc6aaa7543dbef Mon Sep 17 00:00:00 2001 From: Jorge Turrado Date: Wed, 7 Jan 2026 13:37:08 +0100 Subject: [PATCH 12/22] . Signed-off-by: Jorge Turrado --- .github/workflows/tmp.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tmp.yaml b/.github/workflows/tmp.yaml index c9cc03130..5fa4c641c 100644 --- a/.github/workflows/tmp.yaml +++ b/.github/workflows/tmp.yaml @@ -52,6 +52,8 @@ jobs: terraform_wrapper: false - name: Create Test Configuration + env: + STACKIT_USE_OIDC: "true" run: | cat < main.tf terraform { @@ -68,7 +70,6 @@ jobs: service_account_custom_endpoint = "https://service-account.api.qa.stackit.cloud" token_custom_endpoint = "https://accounts.qa.stackit.cloud/oauth/v2/token" service_account_email = "test-idp-njf5pc1@sa.stackit.cloud" - use_oidc = true } resource "stackit_service_account" "sa" { From a10d07aae6609220561f19fdb3c81b813798c51d Mon Sep 17 00:00:00 2001 From: Jorge Turrado Ferrero Date: Wed, 7 Jan 2026 14:22:55 +0100 Subject: [PATCH 13/22] Update tmp.yaml --- .github/workflows/tmp.yaml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tmp.yaml b/.github/workflows/tmp.yaml index 5fa4c641c..70ee75e6e 100644 --- a/.github/workflows/tmp.yaml +++ b/.github/workflows/tmp.yaml @@ -51,9 +51,7 @@ jobs: with: terraform_wrapper: false - - name: Create Test Configuration - env: - STACKIT_USE_OIDC: "true" + - name: Create Test Configuration run: | cat < main.tf terraform { @@ -96,4 +94,4 @@ jobs: terraform destroy -auto-approve else echo "No state file found, skipping destroy." - fi \ No newline at end of file + fi From 3029abd0ae8b231fc9bfc243ad1f37d5c93f755c Mon Sep 17 00:00:00 2001 From: Jorge Turrado Date: Wed, 7 Jan 2026 14:31:15 +0100 Subject: [PATCH 14/22] . Signed-off-by: Jorge Turrado --- stackit/provider.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stackit/provider.go b/stackit/provider.go index 349f52a13..51e8a53ce 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -734,7 +734,7 @@ func githubAssertion(ctx context.Context, oidc_request_url, oidc_request_token s query.Set("audience", "sts.accounts.stackit.cloud") req.URL.RawQuery = query.Encode() } - + req.Header.Set("Accept", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", oidc_request_token)) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") @@ -787,7 +787,7 @@ func getEnvStringOrDefault(val types.String, envVar string, defaultValue string) func getEnvBoolIfValueAbsent(val types.Bool, envVar string) bool { if val.IsNull() || val.IsUnknown() { v := os.Getenv(envVar) - if strings.EqualFold(v, "true") || strings.EqualFold(v, "1") || v == "" { + if strings.EqualFold(v, "true") || strings.EqualFold(v, "1") { return true } } From 7096a0b56fc5a7e986a14c7685146d8692caa309 Mon Sep 17 00:00:00 2001 From: Jorge Turrado Date: Wed, 7 Jan 2026 14:33:20 +0100 Subject: [PATCH 15/22] . Signed-off-by: Jorge Turrado --- .github/workflows/tmp.yaml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tmp.yaml b/.github/workflows/tmp.yaml index 70ee75e6e..295320354 100644 --- a/.github/workflows/tmp.yaml +++ b/.github/workflows/tmp.yaml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - id-token: write + id-token: write steps: - name: Checkout Code @@ -83,9 +83,13 @@ jobs: # Ejecutamos plan. Si el provider carga y la API responde, esto pasará. terraform plan -out=tfplan + env: + STACKIT_USE_OIDC: "1" - name: Terraform Apply run: terraform apply -auto-approve tfplan + env: + STACKIT_USE_OIDC: "1" - name: Terraform Destroy if: always() @@ -95,3 +99,5 @@ jobs: else echo "No state file found, skipping destroy." fi + env: + STACKIT_USE_OIDC: "1" From 7ad472b640ef8732b78dfcdd3ed905363d28e8ad Mon Sep 17 00:00:00 2001 From: Jorge Turrado Date: Wed, 7 Jan 2026 19:09:58 +0100 Subject: [PATCH 16/22] use SDK function Signed-off-by: Jorge Turrado --- go.mod | 2 +- go.sum | 4 +- .../internal/conversion/conversion_test.go | 25 +++++------ stackit/internal/core/core.go | 9 ---- .../access_token/ephemeral_resource.go | 45 +++++-------------- .../access_token/ephemeral_resource_test.go | 9 +++- stackit/provider.go | 40 ++++++++++------- 7 files changed, 58 insertions(+), 76 deletions(-) diff --git a/go.mod b/go.mod index 60b3c097b..aaf4680c8 100644 --- a/go.mod +++ b/go.mod @@ -43,7 +43,7 @@ require ( golang.org/x/mod v0.31.0 ) -replace github.com/stackitcloud/stackit-sdk-go/core => github.com/JorTurFer/stackit-sdk-go/core v0.0.0-20251223142803-960a8fc17ae9 +replace github.com/stackitcloud/stackit-sdk-go/core => github.com/JorTurFer/stackit-sdk-go/core v0.0.0-20260107172957-5e41dc32d226 require ( github.com/hashicorp/go-retryablehttp v0.7.7 // indirect diff --git a/go.sum b/go.sum index 71e4b8c8a..be2186660 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -github.com/JorTurFer/stackit-sdk-go/core v0.0.0-20251223142803-960a8fc17ae9 h1:cbSzCqmQ6SAB5vQrSMN1Zaf1DeGYh5heX3liol8XO2Q= -github.com/JorTurFer/stackit-sdk-go/core v0.0.0-20251223142803-960a8fc17ae9/go.mod h1:fqto7M82ynGhEnpZU6VkQKYWYoFG5goC076JWXTUPRQ= +github.com/JorTurFer/stackit-sdk-go/core v0.0.0-20260107172957-5e41dc32d226 h1:AgqbJ2DrS+Be2Qrgzu2cQYdCFpktG6oo3cFK6XHpBrA= +github.com/JorTurFer/stackit-sdk-go/core v0.0.0-20260107172957-5e41dc32d226/go.mod h1:fqto7M82ynGhEnpZU6VkQKYWYoFG5goC076JWXTUPRQ= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= diff --git a/stackit/internal/conversion/conversion_test.go b/stackit/internal/conversion/conversion_test.go index 4a5593b0f..4a9d42e99 100644 --- a/stackit/internal/conversion/conversion_test.go +++ b/stackit/internal/conversion/conversion_test.go @@ -2,6 +2,8 @@ package conversion import ( "context" + "crypto/tls" + "net/http" "reflect" "testing" @@ -306,6 +308,9 @@ func TestParseProviderData(t *testing.T) { } func TestParseEphemeralProviderData(t *testing.T) { + var randomRoundTripper http.RoundTripper = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } type args struct { providerData any } @@ -354,25 +359,17 @@ func TestParseEphemeralProviderData(t *testing.T) { 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: "", - ServiceAccountFederatedTokenPath: "/home/dev/foo/token", - ServiceAccountEmail: "test@sa.stackit.cloud", + ProviderData: core.ProviderData{ + RoundTripper: randomRoundTripper, + }, }, }, want: want{ ok: true, providerData: core.EphemeralProviderData{ - PrivateKey: "", - PrivateKeyPath: "/home/dev/foo/private-key.json", - ServiceAccountKey: "", - ServiceAccountKeyPath: "/home/dev/foo/key.json", - TokenCustomEndpoint: "", - ServiceAccountFederatedTokenPath: "/home/dev/foo/token", - ServiceAccountEmail: "test@sa.stackit.cloud", + ProviderData: core.ProviderData{ + RoundTripper: randomRoundTripper, + }, }, }, wantErr: false, diff --git a/stackit/internal/core/core.go b/stackit/internal/core/core.go index 1de47f6dd..2f5899d40 100644 --- a/stackit/internal/core/core.go +++ b/stackit/internal/core/core.go @@ -29,15 +29,6 @@ const ( type EphemeralProviderData struct { ProviderData - - PrivateKey string - PrivateKeyPath string - ServiceAccountKey string - ServiceAccountKeyPath string - ServiceAccountFederatedTokenPath string - ServiceAccountFederatedToken string - ServiceAccountEmail string - TokenCustomEndpoint string } type ProviderData struct { diff --git a/stackit/internal/services/access_token/ephemeral_resource.go b/stackit/internal/services/access_token/ephemeral_resource.go index 14b4c9bf7..28d943448 100644 --- a/stackit/internal/services/access_token/ephemeral_resource.go +++ b/stackit/internal/services/access_token/ephemeral_resource.go @@ -3,13 +3,12 @@ package access_token import ( "context" "fmt" + "net/http" "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" @@ -25,7 +24,7 @@ func NewAccessTokenEphemeralResource() ephemeral.EphemeralResource { } type accessTokenEphemeralResource struct { - authConfig config.Configuration + roundTripper http.RoundTripper } func (e *accessTokenEphemeralResource) Configure(ctx context.Context, req ephemeral.ConfigureRequest, resp *ephemeral.ConfigureResponse) { @@ -44,16 +43,7 @@ func (e *accessTokenEphemeralResource) Configure(ctx context.Context, req epheme return } - e.authConfig = config.Configuration{ - ServiceAccountKey: ephemeralProviderData.ServiceAccountKey, - ServiceAccountKeyPath: ephemeralProviderData.ServiceAccountKeyPath, - PrivateKeyPath: ephemeralProviderData.PrivateKey, - PrivateKey: ephemeralProviderData.PrivateKeyPath, - TokenCustomUrl: ephemeralProviderData.TokenCustomEndpoint, - ServiceAccountFederatedTokenPath: ephemeralProviderData.ServiceAccountFederatedTokenPath, - ServiceAccountFederatedToken: ephemeralProviderData.ServiceAccountFederatedToken, - ServiceAccountEmail: ephemeralProviderData.ServiceAccountEmail, - } + e.roundTripper = ephemeralProviderData.RoundTripper } type ephemeralTokenModel struct { @@ -98,7 +88,7 @@ func (e *accessTokenEphemeralResource) Open(ctx context.Context, req ephemeral.O return } - accessToken, err := getAccessToken(&e.authConfig) + accessToken, err := getAccessToken(e.roundTripper) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Access token generation failed", err.Error()) return @@ -108,30 +98,17 @@ func (e *accessTokenEphemeralResource) Open(ctx context.Context, req ephemeral.O 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.SetupAuth(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, - ) - } - +// getAccessToken initializes authentication using the provided config +func getAccessToken(roundTripper http.RoundTripper) (string, error) { // Type assert to access token functionality - var accessToken string - switch client := roundTripper.(type) { - case *clients.KeyFlow: - accessToken, err = client.GetAccessToken() - case *clients.WorkloadIdentityFederationFlow: - accessToken, err = client.GetAccessToken() - default: - return "", fmt.Errorf("internal error: expected KeyFlow or WorkloadIdentityFlow, but received a different implementation of http.RoundTripper") + client, ok := roundTripper.(clients.AuthFlow) + if !ok { + return "", fmt.Errorf("internal error: expected *clients.AuthFlow, but received a different implementation of http.RoundTripper") } + + accessToken, err := client.GetAccessToken() if err != nil { return "", fmt.Errorf("error obtaining access token: %w", err) } - return accessToken, nil } diff --git a/stackit/internal/services/access_token/ephemeral_resource_test.go b/stackit/internal/services/access_token/ephemeral_resource_test.go index c9d1a7531..a468ad97b 100644 --- a/stackit/internal/services/access_token/ephemeral_resource_test.go +++ b/stackit/internal/services/access_token/ephemeral_resource_test.go @@ -13,6 +13,7 @@ import ( "testing" "time" + "github.com/stackitcloud/stackit-sdk-go/core/auth" "github.com/stackitcloud/stackit-sdk-go/core/clients" "github.com/stackitcloud/stackit-sdk-go/core/config" ) @@ -234,7 +235,13 @@ func TestGetAccessToken(t *testing.T) { cfg := tt.cfgFactory() - token, err := getAccessToken(cfg) + roundTripper, err := auth.SetupAuth(cfg) + if tt.expectError { + if err == nil { + t.Errorf("expected error generating round tripper for test case '%s'", tt.description) + } + } + token, err := getAccessToken(roundTripper) if tt.expectError { if err == nil { t.Errorf("expected error but got none for test case '%s'", tt.description) diff --git a/stackit/provider.go b/stackit/provider.go index 51e8a53ce..228e1c724 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -22,6 +22,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types/basetypes" sdkauth "github.com/stackitcloud/stackit-sdk-go/core/auth" "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/core/utils" "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" @@ -466,8 +467,6 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, setStringField(providerConfig.ServiceAccountKeyPath, func(v string) { sdkConfig.ServiceAccountKeyPath = v }) setStringField(providerConfig.PrivateKey, func(v string) { sdkConfig.PrivateKey = v }) setStringField(providerConfig.PrivateKeyPath, func(v string) { sdkConfig.PrivateKeyPath = v }) - setStringField(providerConfig.WifFederatedTokenPath, func(v string) { sdkConfig.ServiceAccountFederatedTokenPath = v }) - setStringField(providerConfig.WifFederatedToken, func(v string) { sdkConfig.ServiceAccountFederatedToken = v }) setBoolField(providerConfig.UseOIDC, func(v bool) { sdkConfig.WorkloadIdentityFederation = v }) setStringField(providerConfig.Token, func(v string) { sdkConfig.Token = v }) setStringField(providerConfig.TokenCustomEndpoint, func(v string) { sdkConfig.TokenCustomUrl = v }) @@ -513,18 +512,36 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, providerData.Experiments = experimentValues } - if getEnvBoolIfValueAbsent(providerConfig.UseOIDC, "STACKIT_USE_OIDC") && sdkConfig.ServiceAccountFederatedToken == "" { + // Workload Identity Federation via provided OIDC Token + oidc_token := "" + setStringField(providerConfig.WifFederatedToken, func(v string) { oidc_token = v }) + if sdkConfig.ServiceAccountFederatedTokenFunc == nil && oidc_token != "" { + sdkConfig.WorkloadIdentityFederation = true + sdkConfig.ServiceAccountFederatedTokenFunc = func() (string, error) { + return oidc_token, nil + } + } + + // Workload Identity Federation via OIDC Token from file + oidc_token_path := "" + setStringField(providerConfig.WifFederatedTokenPath, func(v string) { oidc_token_path = v }) + if sdkConfig.ServiceAccountFederatedTokenFunc == nil && oidc_token_path != "" { + sdkConfig.WorkloadIdentityFederation = true + sdkConfig.ServiceAccountFederatedTokenFunc = func() (string, error) { + return utils.ReadJWTFromFileSystem(oidc_token_path) + } + } + + // Workload Identity Federation via provided OIDC Token from GitHub Actions + if sdkConfig.ServiceAccountFederatedTokenFunc == nil && getEnvBoolIfValueAbsent(providerConfig.UseOIDC, "STACKIT_USE_OIDC") { sdkConfig.WorkloadIdentityFederation = true // https://docs.github.com/en/actions/reference/security/oidc#methods-for-requesting-the-oidc-token oidcReqURL := getEnvStringOrDefault(providerConfig.OIDCTokenRequestURL, "ACTIONS_ID_TOKEN_REQUEST_URL", "") oidcReqToken := getEnvStringOrDefault(providerConfig.OIDCTokenRequestToken, "ACTIONS_ID_TOKEN_REQUEST_TOKEN", "") if oidcReqURL != "" && oidcReqToken != "" { - id_token, err := githubAssertion(ctx, oidcReqURL, oidcReqToken) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring provider", fmt.Sprintf("Requesting id token from Github %v", err)) - return + sdkConfig.ServiceAccountFederatedTokenFunc = func() (string, error) { + return githubAssertion(ctx, oidcReqURL, oidcReqToken) } - sdkConfig.ServiceAccountFederatedToken = id_token } } @@ -543,13 +560,6 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, // 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.ServiceAccountEmail, func(v string) { ephemeralProviderData.ServiceAccountEmail = v }) - 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 }) - setStringField(providerConfig.WifFederatedTokenPath, func(v string) { ephemeralProviderData.ServiceAccountFederatedTokenPath = v }) resp.EphemeralResourceData = ephemeralProviderData providerData.Version = p.version From 218c7a9ab1b0f41c0af6a04d7fea3fa13c5c3159 Mon Sep 17 00:00:00 2001 From: Jorge Turrado Date: Wed, 7 Jan 2026 19:21:44 +0100 Subject: [PATCH 17/22] use SDK function Signed-off-by: Jorge Turrado --- stackit/provider.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackit/provider.go b/stackit/provider.go index 228e1c724..8a5c52abf 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -751,7 +751,7 @@ func githubAssertion(ctx context.Context, oidc_request_url, oidc_request_token s resp, err := http.DefaultClient.Do(req) if err != nil { - return "", fmt.Errorf("githubAssertion: cannot request token: %v", err) + return "", fmt.Errorf("githubAssertion: cannot request token: %v, code: %d", err, resp.StatusCode) } defer resp.Body.Close() From ab6afc4cc2dc33bd610fc627c10b36b13e154b05 Mon Sep 17 00:00:00 2001 From: Jorge Turrado Date: Wed, 7 Jan 2026 19:25:13 +0100 Subject: [PATCH 18/22] use SDK function Signed-off-by: Jorge Turrado --- stackit/provider.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/stackit/provider.go b/stackit/provider.go index 8a5c52abf..2580b9da2 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -9,6 +9,7 @@ import ( "net/url" "os" "strings" + "time" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource" @@ -749,9 +750,12 @@ func githubAssertion(ctx context.Context, oidc_request_url, oidc_request_token s req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", oidc_request_token)) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - resp, err := http.DefaultClient.Do(req) + client := http.DefaultClient + client.Timeout = 3 * time.Second + defer client.CloseIdleConnections() + resp, err := client.Do(req) if err != nil { - return "", fmt.Errorf("githubAssertion: cannot request token: %v, code: %d", err, resp.StatusCode) + return "", fmt.Errorf("githubAssertion: cannot request token: %v", err) } defer resp.Body.Close() From c0c9670b4a6df14fdc2a9076ad6badb1af5fbdfc Mon Sep 17 00:00:00 2001 From: Jorge Turrado Date: Wed, 7 Jan 2026 19:31:21 +0100 Subject: [PATCH 19/22] use SDK function Signed-off-by: Jorge Turrado --- stackit/provider.go | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/stackit/provider.go b/stackit/provider.go index 2580b9da2..bb0ad24df 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -9,7 +9,6 @@ import ( "net/url" "os" "strings" - "time" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource" @@ -541,7 +540,7 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, oidcReqToken := getEnvStringOrDefault(providerConfig.OIDCTokenRequestToken, "ACTIONS_ID_TOKEN_REQUEST_TOKEN", "") if oidcReqURL != "" && oidcReqToken != "" { sdkConfig.ServiceAccountFederatedTokenFunc = func() (string, error) { - return githubAssertion(ctx, oidcReqURL, oidcReqToken) + return githubAssertion(oidcReqURL, oidcReqToken) } } } @@ -730,8 +729,8 @@ func (p *Provider) EphemeralResources(_ context.Context) []func() ephemeral.Ephe } } -func githubAssertion(ctx context.Context, oidc_request_url, oidc_request_token string) (string, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, oidc_request_url, http.NoBody) +func githubAssertion(oidc_request_url, oidc_request_token string) (string, error) { + req, err := http.NewRequest(http.MethodGet, oidc_request_url, http.NoBody) if err != nil { return "", fmt.Errorf("githubAssertion: failed to build request: %+v", err) } @@ -750,10 +749,7 @@ func githubAssertion(ctx context.Context, oidc_request_url, oidc_request_token s req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", oidc_request_token)) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - client := http.DefaultClient - client.Timeout = 3 * time.Second - defer client.CloseIdleConnections() - resp, err := client.Do(req) + resp, err := http.DefaultClient.Do(req) if err != nil { return "", fmt.Errorf("githubAssertion: cannot request token: %v", err) } From 3fc48246608782ddde439528a5b9b84df48d5bbf Mon Sep 17 00:00:00 2001 From: Jorge Turrado Date: Wed, 7 Jan 2026 22:33:19 +0100 Subject: [PATCH 20/22] lint the code Signed-off-by: Jorge Turrado --- stackit/internal/conversion/conversion_test.go | 2 +- stackit/provider.go | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/stackit/internal/conversion/conversion_test.go b/stackit/internal/conversion/conversion_test.go index 4a9d42e99..3bfb042c5 100644 --- a/stackit/internal/conversion/conversion_test.go +++ b/stackit/internal/conversion/conversion_test.go @@ -309,7 +309,7 @@ func TestParseProviderData(t *testing.T) { func TestParseEphemeralProviderData(t *testing.T) { var randomRoundTripper http.RoundTripper = &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS13}, } type args struct { providerData any diff --git a/stackit/provider.go b/stackit/provider.go index bb0ad24df..a411038dc 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -732,7 +732,7 @@ func (p *Provider) EphemeralResources(_ context.Context) []func() ephemeral.Ephe func githubAssertion(oidc_request_url, oidc_request_token string) (string, error) { req, err := http.NewRequest(http.MethodGet, oidc_request_url, http.NoBody) if err != nil { - return "", fmt.Errorf("githubAssertion: failed to build request: %+v", err) + return "", fmt.Errorf("githubAssertion: failed to build request: %w", err) } query, err := url.ParseQuery(req.URL.RawQuery) @@ -751,13 +751,15 @@ func githubAssertion(oidc_request_url, oidc_request_token string) (string, error resp, err := http.DefaultClient.Do(req) if err != nil { - return "", fmt.Errorf("githubAssertion: cannot request token: %v", err) + return "", fmt.Errorf("githubAssertion: cannot request token: %w", err) } - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) if err != nil { - return "", fmt.Errorf("githubAssertion: cannot parse response: %v", err) + return "", fmt.Errorf("githubAssertion: cannot parse response: %w", err) } if c := resp.StatusCode; c < 200 || c > 299 { @@ -768,7 +770,7 @@ func githubAssertion(oidc_request_url, oidc_request_token string) (string, error Value string `json:"value"` } if err := json.Unmarshal(body, &tokenRes); err != nil { - return "", fmt.Errorf("githubAssertion: cannot unmarshal response: %v", err) + return "", fmt.Errorf("githubAssertion: cannot unmarshal response: %w", err) } return tokenRes.Value, nil @@ -777,7 +779,7 @@ func githubAssertion(oidc_request_url, oidc_request_token string) (string, error // getEnvStringOrDefault takes a Framework StringValue and a corresponding Environment Variable name and returns // either the string value set in the StringValue if not Null / Unknown _or_ the os.GetEnv() value of the Environment // Variable provided. If both of these are empty, an empty string defaultValue is returned. -func getEnvStringOrDefault(val types.String, envVar string, defaultValue string) string { +func getEnvStringOrDefault(val types.String, envVar, defaultValue string) string { if val.IsNull() || val.IsUnknown() { if v := os.Getenv(envVar); v != "" { return os.Getenv(envVar) From 37a63ee6eff4eaebbd3cd45bd474e4bfa370d9ef Mon Sep 17 00:00:00 2001 From: Jorge Turrado Date: Wed, 7 Jan 2026 22:41:43 +0100 Subject: [PATCH 21/22] update docs Signed-off-by: Jorge Turrado --- docs/index.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 095ddaeb7..a85f49462 100644 --- a/docs/index.md +++ b/docs/index.md @@ -170,6 +170,8 @@ Note: AWS specific checks must be skipped as they do not work on STACKIT. For de - `mongodbflex_custom_endpoint` (String) Custom endpoint for the MongoDB Flex service - `objectstorage_custom_endpoint` (String) Custom endpoint for the Object Storage service - `observability_custom_endpoint` (String) Custom endpoint for the Observability service +- `oidc_request_token` (String) The bearer token for the request to the OIDC provider. For use when authenticating as a Service Account using OpenID Connect. +- `oidc_request_url` (String) The URL for the OIDC provider from which to request an ID token. For use when authenticating as a Service Account using OpenID Connect. - `opensearch_custom_endpoint` (String) Custom endpoint for the OpenSearch service - `postgresflex_custom_endpoint` (String) Custom endpoint for the PostgresFlex service - `private_key` (String) Private RSA key used for authentication, relevant for the key flow. It takes precedence over the private key that is included in the service account key. @@ -183,7 +185,9 @@ Note: AWS specific checks must be skipped as they do not work on STACKIT. For de - `server_backup_custom_endpoint` (String) Custom endpoint for the Server Backup service - `server_update_custom_endpoint` (String) Custom endpoint for the Server Update service - `service_account_custom_endpoint` (String) Custom endpoint for the Service Account service -- `service_account_email` (String, Deprecated) Service account email. It can also be set using the environment variable STACKIT_SERVICE_ACCOUNT_EMAIL. It is required if you want to use the resource manager project resource. +- `service_account_email` (String) Service account email. It can also be set using the environment variable STACKIT_SERVICE_ACCOUNT_EMAIL. It is required if you want to use the resource manager project resource. +- `service_account_federated_token` (String) The OIDC ID token for use when authenticating as a Service Account using OpenID Connect. +- `service_account_federated_token_path` (String) Path for workload identity assertion. It can also be set using the environment variable STACKIT_FEDERATED_TOKEN_FILE. - `service_account_key` (String) Service account key used for authentication. If set, the key flow will be used to authenticate all operations. - `service_account_key_path` (String) Path for the service account key used for authentication. If set, the key flow will be used to authenticate all operations. - `service_account_token` (String, Deprecated) Token used for authentication. If set, the token flow will be used to authenticate all operations. @@ -192,3 +196,4 @@ Note: AWS specific checks must be skipped as they do not work on STACKIT. For de - `ske_custom_endpoint` (String) Custom endpoint for the Kubernetes Engine (SKE) service - `sqlserverflex_custom_endpoint` (String) Custom endpoint for the SQL Server Flex service - `token_custom_endpoint` (String) Custom endpoint for the token API, which is used to request access tokens when using the key flow +- `use_oidc` (Boolean) Should OIDC be used for Authentication? This can also be sourced from the `STACKIT_USE_OIDC` Environment Variable. Defaults to `false`. From 7538212f4f2e4a081a979cddbfbf9897eea05d53 Mon Sep 17 00:00:00 2001 From: Jorge Turrado Date: Mon, 12 Jan 2026 18:38:30 +0100 Subject: [PATCH 22/22] bump sdk Signed-off-by: Jorge Turrado --- go.mod | 2 +- go.sum | 4 +-- stackit/provider.go | 63 +++------------------------------------------ 3 files changed, 7 insertions(+), 62 deletions(-) diff --git a/go.mod b/go.mod index aaf4680c8..97e4984e9 100644 --- a/go.mod +++ b/go.mod @@ -43,7 +43,7 @@ require ( golang.org/x/mod v0.31.0 ) -replace github.com/stackitcloud/stackit-sdk-go/core => github.com/JorTurFer/stackit-sdk-go/core v0.0.0-20260107172957-5e41dc32d226 +replace github.com/stackitcloud/stackit-sdk-go/core => github.com/JorTurFer/stackit-sdk-go/core v0.0.0-20260112173602-1da4767050f7 require ( github.com/hashicorp/go-retryablehttp v0.7.7 // indirect diff --git a/go.sum b/go.sum index be2186660..ece407e9c 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -github.com/JorTurFer/stackit-sdk-go/core v0.0.0-20260107172957-5e41dc32d226 h1:AgqbJ2DrS+Be2Qrgzu2cQYdCFpktG6oo3cFK6XHpBrA= -github.com/JorTurFer/stackit-sdk-go/core v0.0.0-20260107172957-5e41dc32d226/go.mod h1:fqto7M82ynGhEnpZU6VkQKYWYoFG5goC076JWXTUPRQ= +github.com/JorTurFer/stackit-sdk-go/core v0.0.0-20260112173602-1da4767050f7 h1:Uhu4py2YXe6302u7naUIcn2TnxpnpwM+QGSh9Sj7xFk= +github.com/JorTurFer/stackit-sdk-go/core v0.0.0-20260112173602-1da4767050f7/go.mod h1:fqto7M82ynGhEnpZU6VkQKYWYoFG5goC076JWXTUPRQ= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= diff --git a/stackit/provider.go b/stackit/provider.go index a411038dc..4aa182a78 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -2,11 +2,7 @@ package stackit import ( "context" - "encoding/json" "fmt" - "io" - "net/http" - "net/url" "os" "strings" @@ -22,7 +18,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types/basetypes" sdkauth "github.com/stackitcloud/stackit-sdk-go/core/auth" "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/core/oidcadapters" "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" @@ -517,7 +513,7 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, setStringField(providerConfig.WifFederatedToken, func(v string) { oidc_token = v }) if sdkConfig.ServiceAccountFederatedTokenFunc == nil && oidc_token != "" { sdkConfig.WorkloadIdentityFederation = true - sdkConfig.ServiceAccountFederatedTokenFunc = func() (string, error) { + sdkConfig.ServiceAccountFederatedTokenFunc = func(context.Context) (string, error) { return oidc_token, nil } } @@ -527,9 +523,7 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, setStringField(providerConfig.WifFederatedTokenPath, func(v string) { oidc_token_path = v }) if sdkConfig.ServiceAccountFederatedTokenFunc == nil && oidc_token_path != "" { sdkConfig.WorkloadIdentityFederation = true - sdkConfig.ServiceAccountFederatedTokenFunc = func() (string, error) { - return utils.ReadJWTFromFileSystem(oidc_token_path) - } + sdkConfig.ServiceAccountFederatedTokenFunc = oidcadapters.ReadJWTFromFileSystem(oidc_token_path) } // Workload Identity Federation via provided OIDC Token from GitHub Actions @@ -539,9 +533,7 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, oidcReqURL := getEnvStringOrDefault(providerConfig.OIDCTokenRequestURL, "ACTIONS_ID_TOKEN_REQUEST_URL", "") oidcReqToken := getEnvStringOrDefault(providerConfig.OIDCTokenRequestToken, "ACTIONS_ID_TOKEN_REQUEST_TOKEN", "") if oidcReqURL != "" && oidcReqToken != "" { - sdkConfig.ServiceAccountFederatedTokenFunc = func() (string, error) { - return githubAssertion(oidcReqURL, oidcReqToken) - } + sdkConfig.ServiceAccountFederatedTokenFunc = oidcadapters.RequestGHOIDCToken(oidcReqURL, oidcReqToken) } } @@ -729,53 +721,6 @@ func (p *Provider) EphemeralResources(_ context.Context) []func() ephemeral.Ephe } } -func githubAssertion(oidc_request_url, oidc_request_token string) (string, error) { - req, err := http.NewRequest(http.MethodGet, oidc_request_url, http.NoBody) - if err != nil { - return "", fmt.Errorf("githubAssertion: failed to build request: %w", err) - } - - query, err := url.ParseQuery(req.URL.RawQuery) - if err != nil { - return "", fmt.Errorf("githubAssertion: cannot parse URL query") - } - - if query.Get("audience") == "" { - query.Set("audience", "sts.accounts.stackit.cloud") - req.URL.RawQuery = query.Encode() - } - - req.Header.Set("Accept", "application/json") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", oidc_request_token)) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return "", fmt.Errorf("githubAssertion: cannot request token: %w", err) - } - - defer func() { - _ = resp.Body.Close() - }() - body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) - if err != nil { - return "", fmt.Errorf("githubAssertion: cannot parse response: %w", err) - } - - if c := resp.StatusCode; c < 200 || c > 299 { - return "", fmt.Errorf("githubAssertion: received HTTP status %d with response: %s", resp.StatusCode, body) - } - - var tokenRes struct { - Value string `json:"value"` - } - if err := json.Unmarshal(body, &tokenRes); err != nil { - return "", fmt.Errorf("githubAssertion: cannot unmarshal response: %w", err) - } - - return tokenRes.Value, nil -} - // getEnvStringOrDefault takes a Framework StringValue and a corresponding Environment Variable name and returns // either the string value set in the StringValue if not Null / Unknown _or_ the os.GetEnv() value of the Environment // Variable provided. If both of these are empty, an empty string defaultValue is returned.