diff --git a/stackit/internal/core/core.go b/stackit/internal/core/core.go index 68240b74..f9d224fd 100644 --- a/stackit/internal/core/core.go +++ b/stackit/internal/core/core.go @@ -34,6 +34,7 @@ type ProviderData struct { SecretsManagerCustomEndpoint string SQLServerFlexCustomEndpoint string SKECustomEndpoint string + EnableBetaResources bool } // DiagsToError Converts TF diagnostics' errors into an error with a human-readable description. @@ -66,3 +67,17 @@ func LogAndAddWarning(ctx context.Context, diags *diag.Diagnostics, summary, det tflog.Warn(ctx, fmt.Sprintf("%s | %s", summary, detail)) diags.AddWarning(summary, detail) } + +func LogAndAddWarningBeta(ctx context.Context, diags *diag.Diagnostics, name string) { + warnTitle := fmt.Sprintf("The resource %q is in BETA", name) + warnContent := fmt.Sprintf("The resource %q is in BETA and may be subject to breaking changes in the future. Use with caution.", name) + tflog.Warn(ctx, fmt.Sprintf("%s | %s", warnTitle, warnContent)) + diags.AddWarning(warnTitle, warnContent) +} + +func LogAndAddErrorBeta(ctx context.Context, diags *diag.Diagnostics, name string) { + errTitle := fmt.Sprintf("The resource %q is in BETA and BETA is not enabled", name) + errContent := fmt.Sprintf("The resource %q is in BETA and the BETA functionality is currently not enabled. Please refer to the documentation on how to enable the BETA functionality.", name) + tflog.Error(ctx, fmt.Sprintf("%s | %s", errTitle, errContent)) + diags.AddError(errTitle, errContent) +} diff --git a/stackit/internal/features/beta.go b/stackit/internal/features/beta.go new file mode 100644 index 00000000..f072fa87 --- /dev/null +++ b/stackit/internal/features/beta.go @@ -0,0 +1,48 @@ +package features + +import ( + "context" + "fmt" + "os" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" +) + +// BetaResourcesEnabled returns whether this provider has BETA functionality enabled. +// +// In order of precedence, beta functionality can be managed by: +// - Environment Variable `STACKIT_TF_ENABLE_BETA_RESOURCES` - `true` is enabled, `false` is disabled. +// - Provider configuration feature flag `enable_beta` - `true` is enabled, `false` is disabled. +func BetaResourcesEnabled(ctx context.Context, data *core.ProviderData, diags *diag.Diagnostics) bool { + value, set := os.LookupEnv("STACKIT_TF_ENABLE_BETA_RESOURCES") + if set { + if strings.EqualFold(value, "true") { + return true + } + if strings.EqualFold(value, "false") { + return false + } + warnDetails := fmt.Sprintf(`The value of the environment variable that enables BETA functionality must be either "true" or "false", got %q. +Defaulting to the provider feature flag.`, value) + core.LogAndAddWarning(ctx, diags, "Invalid value for STACKIT_TF_ENABLE_BETA_RESOURCES environment variable.", warnDetails) + } + // ProviderData should always be set, but we check just in case + if data == nil { + return false + } + return data.EnableBetaResources +} + +// CheckBetaResourcesEnabled is a helper function to log and add a warning or error if the BETA functionality is not enabled. +// +// Should be called in the Configure method of a BETA resource. +// Then, check for Errors in the diags using the diags.HasError() method. +func CheckBetaResourcesEnabled(ctx context.Context, data *core.ProviderData, diags *diag.Diagnostics, resourceName string) { + if !BetaResourcesEnabled(ctx, data, diags) { + core.LogAndAddErrorBeta(ctx, diags, resourceName) + return + } + core.LogAndAddWarningBeta(ctx, diags, resourceName) +} diff --git a/stackit/internal/features/beta_test.go b/stackit/internal/features/beta_test.go new file mode 100644 index 00000000..0e34c630 --- /dev/null +++ b/stackit/internal/features/beta_test.go @@ -0,0 +1,216 @@ +package features + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" +) + +func TestBetaResourcesEnabled(t *testing.T) { + tests := []struct { + description string + data *core.ProviderData + envSet bool + envValue string + expected bool + expectWarn bool + }{ + { + description: "Feature flag enabled, env var not set", + data: &core.ProviderData{ + EnableBetaResources: true, + }, + expected: true, + }, + { + description: "Feature flag is disabled, env var not set", + data: &core.ProviderData{ + EnableBetaResources: false, + }, + expected: false, + }, + { + description: "Feature flag, Env var not set", + data: &core.ProviderData{}, + expected: false, + }, + { + description: "Feature flag not set, Env var is true", + data: &core.ProviderData{}, + envSet: true, + envValue: "true", + expected: true, + }, + { + description: "Feature flag not set, Env var is false", + data: &core.ProviderData{}, + envSet: true, + envValue: "false", + expected: false, + }, + { + description: "Feature flag not set, Env var is empty", + data: &core.ProviderData{}, + envSet: true, + envValue: "", + expectWarn: true, + expected: false, + }, + { + description: "Feature flag not set, Env var is gibberish", + data: &core.ProviderData{}, + envSet: true, + envValue: "gibberish", + expectWarn: true, + expected: false, + }, + { + description: "Feature flag enabled, Env var is true", + data: &core.ProviderData{ + EnableBetaResources: true, + }, + envSet: true, + envValue: "true", + expected: true, + }, + { + description: "Feature flag enabled, Env var is false", + data: &core.ProviderData{ + EnableBetaResources: true, + }, + envSet: true, + envValue: "false", + expected: false, + }, + { + description: "Feature flag enabled, Env var is empty", + data: &core.ProviderData{ + EnableBetaResources: true, + }, + envSet: true, + envValue: "", + expectWarn: true, + expected: true, + }, + { + description: "Feature flag enabled, Env var is gibberish", + data: &core.ProviderData{ + EnableBetaResources: true, + }, + envSet: true, + envValue: "gibberish", + expectWarn: true, + expected: true, + }, + { + description: "Feature flag disabled, Env var is true", + data: &core.ProviderData{ + EnableBetaResources: false, + }, + envSet: true, + envValue: "true", + expected: true, + }, + { + description: "Feature flag disabled, Env var is false", + data: &core.ProviderData{ + EnableBetaResources: false, + }, + envSet: true, + envValue: "false", + expected: false, + }, + { + description: "Feature flag disabled, Env var is empty", + data: &core.ProviderData{ + EnableBetaResources: false, + }, + envSet: true, + envValue: "", + expectWarn: true, + expected: false, + }, + { + description: "Feature flag disabled, Env var is gibberish", + data: &core.ProviderData{ + EnableBetaResources: false, + }, + envSet: true, + envValue: "gibberish", + expectWarn: true, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + if tt.envSet { + t.Setenv("STACKIT_TF_ENABLE_BETA_RESOURCES", tt.envValue) + } + diags := diag.Diagnostics{} + + result := BetaResourcesEnabled(context.Background(), tt.data, &diags) + if result != tt.expected { + t.Fatalf("Expected %t, got %t", tt.expected, result) + } + + if tt.expectWarn && diags.WarningsCount() == 0 { + t.Fatalf("Expected warning, got none") + } + if !tt.expectWarn && diags.WarningsCount() > 0 { + t.Fatalf("Expected no warning, got %d", diags.WarningsCount()) + } + }) + } +} + +func TestCheckBetaResourcesEnabled(t *testing.T) { + tests := []struct { + description string + betaEnabled bool + expectError bool + expectWarn bool + }{ + { + description: "Beta enabled, show warning", + betaEnabled: true, + expectWarn: true, + }, + { + description: "Beta disabled, show error", + betaEnabled: false, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + var envValue string + if tt.betaEnabled { + envValue = "true" + } else { + envValue = "false" + } + t.Setenv("STACKIT_TF_ENABLE_BETA_RESOURCES", envValue) + + diags := diag.Diagnostics{} + CheckBetaResourcesEnabled(context.Background(), &core.ProviderData{}, &diags, "test") + + if tt.expectError && diags.ErrorsCount() == 0 { + t.Fatalf("Expected error, got none") + } + if !tt.expectError && diags.ErrorsCount() > 0 { + t.Fatalf("Expected no error, got %d", diags.ErrorsCount()) + } + + if tt.expectWarn && diags.WarningsCount() == 0 { + t.Fatalf("Expected warning, got none") + } + if !tt.expectWarn && diags.WarningsCount() > 0 { + t.Fatalf("Expected no warning, got %d", diags.WarningsCount()) + } + }) + } +} diff --git a/stackit/provider.go b/stackit/provider.go index b3513c19..e07f5bd0 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -103,6 +103,7 @@ type providerModel struct { ResourceManagerCustomEndpoint types.String `tfsdk:"resourcemanager_custom_endpoint"` TokenCustomEndpoint types.String `tfsdk:"token_custom_endpoint"` JWKSCustomEndpoint types.String `tfsdk:"jwks_custom_endpoint"` + EnableBetaResources types.Bool `tfsdk:"enable_beta_resources"` } // Schema defines the provider-level schema for configuration data. @@ -135,6 +136,7 @@ func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *pro "ske_custom_endpoint": "Custom endpoint for the Kubernetes Engine (SKE) service", "token_custom_endpoint": "Custom endpoint for the token API, which is used to request access tokens when using the key flow", "jwks_custom_endpoint": "Custom endpoint for the jwks API, which is used to get the json web key sets (jwks) to validate tokens when using the key flow", + "enable_beta_resources": "Enable beta resources. Default is false.", } resp.Schema = schema.Schema{ @@ -248,6 +250,10 @@ func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *pro Description: descriptions["jwks_custom_endpoint"], DeprecationMessage: "Validation using JWKS was removed, for being redundant with token validation done in the APIs. This field has no effect, and will be removed in a later update", }, + "enable_beta_resources": schema.BoolAttribute{ + Optional: true, + Description: descriptions["enable_beta_resources"], + }, }, } } @@ -344,6 +350,9 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, if !(providerConfig.TokenCustomEndpoint.IsUnknown() || providerConfig.TokenCustomEndpoint.IsNull()) { sdkConfig.TokenCustomUrl = providerConfig.TokenCustomEndpoint.ValueString() } + if !(providerConfig.EnableBetaResources.IsUnknown() || providerConfig.EnableBetaResources.IsNull()) { + providerData.EnableBetaResources = providerConfig.EnableBetaResources.ValueBool() + } roundTripper, err := sdkauth.SetupAuth(sdkConfig) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring provider", fmt.Sprintf("Setting up authentication: %v", err))