diff --git a/README.md b/README.md index 4f087aad7..a5215d947 100644 --- a/README.md +++ b/README.md @@ -195,7 +195,7 @@ To enable experiments set the experiments field in the provider definition: ```hcl provider "stackit" { default_region = "eu01" - experiments = ["iam", "routing-tables", "network"] + experiments = ["iam", "routing-tables", "network", "dremio", "ske"] } ``` @@ -221,6 +221,10 @@ Enables the usage and provisioning of STACKIT Dremio resources. The STACKIT Dremio API is currently in alpha state. The fields of the resources are still subject to change. +#### `ske` + +Enables experimental SKE features. These features are subject to change and may be removed or redesigned in future releases. + ## Acceptance Tests > [!WARNING] diff --git a/docs/ephemeral-resources/ske_kubeconfig.md b/docs/ephemeral-resources/ske_kubeconfig.md new file mode 100644 index 000000000..0f3a8a39a --- /dev/null +++ b/docs/ephemeral-resources/ske_kubeconfig.md @@ -0,0 +1,59 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_ske_kubeconfig Ephemeral Resource - stackit" +subcategory: "" +description: |- + Ephemeral resource that generates a short-lived SKE kubeconfig. A new kubeconfig is generated each time the resource is evaluated, and it remains consistent for the duration of a Terraform operation. + ~> This ephemeral-resource is part of the experimental feature ske and is likely going to undergo significant changes or be removed in the future. Use it at your own discretion. +--- + +# stackit_ske_kubeconfig (Ephemeral Resource) + +Ephemeral resource that generates a short-lived SKE kubeconfig. A new kubeconfig is generated each time the resource is evaluated, and it remains consistent for the duration of a Terraform operation. + +~> This ephemeral-resource is part of the experimental feature ske and is likely going to undergo significant changes or be removed in the future. Use it at your own discretion. + +## Example Usage + +```terraform +provider "stackit" { + experiments = ["ske"] +} + +resource "stackit_ske_cluster" "example" { + # ... cluster configuration ... +} + +# We use the cluster ID ternary to force evaluation during the Apply phase. +# Unlike managed resources, ephemeral resources evaluate during the Plan phase +# if inputs are known, which would trigger a 404 before the cluster exists. +ephemeral "stackit_ske_kubeconfig" "example" { + project_id = stackit_ske_cluster.example.project_id + cluster_name = stackit_ske_cluster.example.id != "" ? stackit_ske_cluster.example.name : "" +} + +provider "kubernetes" { + host = yamldecode(ephemeral.stackit_ske_kubeconfig.example.kube_config).clusters.0.cluster.server + client_certificate = base64decode(yamldecode(ephemeral.stackit_ske_kubeconfig.example.kube_config).users.0.user.client-certificate-data) + client_key = base64decode(yamldecode(ephemeral.stackit_ske_kubeconfig.example.kube_config).users.0.user.client-key-data) + cluster_ca_certificate = base64decode(yamldecode(ephemeral.stackit_ske_kubeconfig.example.kube_config).clusters.0.cluster.certificate-authority-data) +} +``` + + +## Schema + +### Required + +- `cluster_name` (String) Name of the SKE cluster. +- `project_id` (String) STACKIT project ID to which the cluster is associated. + +### Optional + +- `expiration` (Number) Expiration time of the kubeconfig in seconds. Must be between `600` (10m) and `14400` (4h). API defaults to `3600` (1h). +- `region` (String) The resource region. If not defined, the provider region is used. + +### Read-Only + +- `expires_at` (String) Timestamp when the kubeconfig expires. +- `kube_config` (String, Sensitive) Raw short-lived admin kubeconfig. diff --git a/docs/index.md b/docs/index.md index c3ceba33e..4c0623665 100644 --- a/docs/index.md +++ b/docs/index.md @@ -182,7 +182,7 @@ See this [example](https://professional-service.git.onstackit.cloud/professional - `dremio_custom_endpoint` (String) Custom endpoint for the Dremio service - `edgecloud_custom_endpoint` (String) Custom endpoint for the Edge Cloud service - `enable_beta_resources` (Boolean) Enable beta resources. Default is false. -- `experiments` (List of String) Enables experiments. These are unstable features without official support. More information can be found in the README. Available Experiments: dremio, iam, network, routing-tables, vpc +- `experiments` (List of String) Enables experiments. These are unstable features without official support. More information can be found in the README. Available Experiments: dremio, iam, network, routing-tables, vpc, ske - `git_custom_endpoint` (String) Custom endpoint for the Git service - `iaas_custom_endpoint` (String) Custom endpoint for the IaaS service - `intake_custom_endpoint` (String) Custom endpoint for the Intake service diff --git a/examples/ephemeral-resources/stackit_ske_kubeconfig/ephemeral-resource.tf b/examples/ephemeral-resources/stackit_ske_kubeconfig/ephemeral-resource.tf new file mode 100644 index 000000000..077c98d4e --- /dev/null +++ b/examples/ephemeral-resources/stackit_ske_kubeconfig/ephemeral-resource.tf @@ -0,0 +1,22 @@ +provider "stackit" { + experiments = ["ske"] +} + +resource "stackit_ske_cluster" "example" { + # ... cluster configuration ... +} + +# We use the cluster ID ternary to force evaluation during the Apply phase. +# Unlike managed resources, ephemeral resources evaluate during the Plan phase +# if inputs are known, which would trigger a 404 before the cluster exists. +ephemeral "stackit_ske_kubeconfig" "example" { + project_id = stackit_ske_cluster.example.project_id + cluster_name = stackit_ske_cluster.example.id != "" ? stackit_ske_cluster.example.name : "" +} + +provider "kubernetes" { + host = yamldecode(ephemeral.stackit_ske_kubeconfig.example.kube_config).clusters.0.cluster.server + client_certificate = base64decode(yamldecode(ephemeral.stackit_ske_kubeconfig.example.kube_config).users.0.user.client-certificate-data) + client_key = base64decode(yamldecode(ephemeral.stackit_ske_kubeconfig.example.kube_config).users.0.user.client-key-data) + cluster_ca_certificate = base64decode(yamldecode(ephemeral.stackit_ske_kubeconfig.example.kube_config).clusters.0.cluster.certificate-authority-data) +} diff --git a/stackit/internal/features/experiments.go b/stackit/internal/features/experiments.go index 902e6ecad..3cac7b737 100644 --- a/stackit/internal/features/experiments.go +++ b/stackit/internal/features/experiments.go @@ -18,9 +18,10 @@ const ( IamExperiment = "iam" DremioExperiment = "dremio" VpcExperiment = "vpc" + SkeExperiment = "ske" ) -var AvailableExperiments = []string{DremioExperiment, IamExperiment, NetworkExperiment, RoutingTablesExperiment, VpcExperiment} +var AvailableExperiments = []string{DremioExperiment, IamExperiment, NetworkExperiment, RoutingTablesExperiment, VpcExperiment, SkeExperiment} // Check if an experiment is valid. func ValidExperiment(experiment string, diags *diag.Diagnostics) bool { diff --git a/stackit/internal/services/access_token/access_token_acc_test.go b/stackit/internal/services/access_token/access_token_acc_test.go index 47a5ee049..529e8fd92 100644 --- a/stackit/internal/services/access_token/access_token_acc_test.go +++ b/stackit/internal/services/access_token/access_token_acc_test.go @@ -33,6 +33,7 @@ func TestAccEphemeralAccessToken(t *testing.T) { Config: ephemeralResourceConfig, ConfigVariables: testConfigVars, ConfigStateChecks: []statecheck.StateCheck{ + // Check that the output is not null statecheck.ExpectKnownValue( "echo.example", tfjsonpath.New("data").AtMapKey("access_token"), @@ -42,7 +43,7 @@ func TestAccEphemeralAccessToken(t *testing.T) { statecheck.ExpectKnownValue( "echo.example", tfjsonpath.New("data").AtMapKey("access_token"), - knownvalue.StringRegexp(regexp.MustCompile(`^ey`)), + knownvalue.StringRegexp(regexp.MustCompile("^ey")), ), }, }, diff --git a/stackit/internal/services/ske/kubeconfig/ephemeral_resource.go b/stackit/internal/services/ske/kubeconfig/ephemeral_resource.go new file mode 100644 index 000000000..1a441a337 --- /dev/null +++ b/stackit/internal/services/ske/kubeconfig/ephemeral_resource.go @@ -0,0 +1,176 @@ +package ske + +import ( + "context" + "fmt" + "strconv" + "time" + + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" + + "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" + skeUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/ske/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ ephemeral.EphemeralResource = &kubeconfigEphemeralResource{} + _ ephemeral.EphemeralResourceWithConfigure = &kubeconfigEphemeralResource{} +) + +// NewKubeconfigEphemeralResource is a helper function to simplify the provider implementation. +func NewKubeconfigEphemeralResource() ephemeral.EphemeralResource { + return &kubeconfigEphemeralResource{} +} + +// kubeconfigEphemeralResource is the ephemeral resource implementation. +type kubeconfigEphemeralResource struct { + client *ske.APIClient + providerData core.ProviderData +} + +// Metadata returns the resource type name. +func (e *kubeconfigEphemeralResource) Metadata(_ context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_ske_kubeconfig" +} + +// Configure adds the provider configured client to the resource. +func (e *kubeconfigEphemeralResource) Configure(ctx context.Context, req ephemeral.ConfigureRequest, resp *ephemeral.ConfigureResponse) { + ephemeralProviderData, ok := conversion.ParseEphemeralProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + e.providerData = ephemeralProviderData.ProviderData + + features.CheckExperimentEnabled(ctx, &e.providerData, features.SkeExperiment, "stackit_ske_kubeconfig", core.EphemeralResource, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + e.client = skeUtils.ConfigureClient(ctx, &e.providerData, &resp.Diagnostics) + + tflog.Info(ctx, "SKE kubeconfig client configured") +} + +// ephemeralModel is the model for the ephemeral resource. +type ephemeralModel struct { + ClusterName types.String `tfsdk:"cluster_name"` + ProjectId types.String `tfsdk:"project_id"` + Expiration types.Int64 `tfsdk:"expiration"` + Region types.String `tfsdk:"region"` + Kubeconfig types.String `tfsdk:"kube_config"` + ExpiresAt types.String `tfsdk:"expires_at"` +} + +// Schema defines the schema for the ephemeral resource. +func (e *kubeconfigEphemeralResource) Schema(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + description := features.AddExperimentDescription( + "Ephemeral resource that generates a short-lived SKE kubeconfig. "+ + "A new kubeconfig is generated each time the resource is evaluated, and it remains consistent for the duration of a Terraform operation.", + features.SkeExperiment, + core.EphemeralResource, + ) + + resp.Schema = schema.Schema{ + Description: description, + Attributes: map[string]schema.Attribute{ + "cluster_name": schema.StringAttribute{ + Description: "Name of the SKE cluster.", + Required: true, + Validators: []validator.String{ + validate.NoSeparator(), + }, + }, + "project_id": schema.StringAttribute{ + Description: "STACKIT project ID to which the cluster is associated.", + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "expiration": schema.Int64Attribute{ + Description: "Expiration time of the kubeconfig in seconds. Must be between `600` (10m) and `14400` (4h). " + + "API defaults to `3600` (1h).", + Optional: true, + Validators: []validator.Int64{ + int64validator.AtLeast(600), + int64validator.AtMost(14400), + }, + }, + "region": schema.StringAttribute{ + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + Description: "The resource region. If not defined, the provider region is used.", + }, + "kube_config": schema.StringAttribute{ + Description: "Raw short-lived admin kubeconfig.", + Computed: true, + Sensitive: true, + }, + "expires_at": schema.StringAttribute{ + Description: "Timestamp when the kubeconfig expires.", + Computed: true, + }, + }, + } +} + +// Open creates the kubeconfig and sets the result. +func (e *kubeconfigEphemeralResource) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + var model ephemeralModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + clusterName := model.ClusterName.ValueString() + region := e.providerData.GetRegionWithOverride(model.Region) + + kubeconfigResp, err := getKubeconfig(ctx, e.client.DefaultAPI, projectId, region, clusterName, conversion.Int64ValueToPointer(model.Expiration)) + + ctx = core.LogResponse(ctx) + + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating kubeconfig", fmt.Sprintf("Calling SKE API: %v", err)) + return + } + + if kubeconfigResp == nil || kubeconfigResp.Kubeconfig == nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating kubeconfig", "API returned an empty response") + return + } + + model.Kubeconfig = types.StringPointerValue(kubeconfigResp.Kubeconfig) + model.ExpiresAt = types.StringValue(kubeconfigResp.ExpirationTimestamp.Format(time.RFC3339)) + model.Region = types.StringValue(region) + + resp.Diagnostics.Append(resp.Result.Set(ctx, model)...) + tflog.Info(ctx, "SKE kubeconfig opened") +} + +// getKubeconfig initializes the API call to generate a new kubeconfig +func getKubeconfig(ctx context.Context, client ske.DefaultAPI, projectId, region, clusterName string, expiration *int64) (*ske.Kubeconfig, error) { + payload := ske.CreateKubeconfigPayload{} + if expiration != nil { + expirationStr := strconv.FormatInt(*expiration, 10) + payload.ExpirationSeconds = &expirationStr + } + + return client.CreateKubeconfig(ctx, projectId, region, clusterName).CreateKubeconfigPayload(payload).Execute() +} diff --git a/stackit/internal/services/ske/kubeconfig/ephemeral_resource_test.go b/stackit/internal/services/ske/kubeconfig/ephemeral_resource_test.go new file mode 100644 index 000000000..b6ab20642 --- /dev/null +++ b/stackit/internal/services/ske/kubeconfig/ephemeral_resource_test.go @@ -0,0 +1,80 @@ +package ske + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" +) + +func TestGetKubeconfig(t *testing.T) { + const ( + projectId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + clusterName = "cluster" + region = "eu01" + kubeconfig = "mock-kubeconfig" + ) + expirationTime := time.Now().Add(time.Hour).Truncate(time.Second) + + tests := []struct { + description string + expiration *int64 + mockResponse *ske.Kubeconfig + mockError error + expectError bool + }{ + { + description: "success without expiration", + expiration: nil, + mockResponse: &ske.Kubeconfig{ + Kubeconfig: &[]string{kubeconfig}[0], + ExpirationTimestamp: &expirationTime, + AdditionalProperties: make(map[string]any), + }, + expectError: false, + }, + { + description: "success with expiration", + expiration: &[]int64{3600}[0], + mockResponse: &ske.Kubeconfig{ + Kubeconfig: &[]string{kubeconfig}[0], + ExpirationTimestamp: &expirationTime, + AdditionalProperties: make(map[string]any), + }, + expectError: false, + }, + { + description: "api error", + mockError: fmt.Errorf("api error"), + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + mockResp := tt.mockResponse + mockErr := tt.mockError + createKubeconfigFn := func(_ ske.ApiCreateKubeconfigRequest) (*ske.Kubeconfig, error) { + return mockResp, mockErr + } + client := &ske.DefaultAPIServiceMock{ + CreateKubeconfigExecuteMock: &createKubeconfigFn, + } + + resp, err := getKubeconfig(context.Background(), client, projectId, region, clusterName, tt.expiration) + + if (err != nil) != tt.expectError { + t.Fatalf("getKubeconfig() error = %v, expectError %v", err, tt.expectError) + } + + if !tt.expectError { + if diff := cmp.Diff(resp, tt.mockResponse); diff != "" { + t.Errorf("Response mismatch (-want +got):\n%s", diff) + } + } + }) + } +} diff --git a/stackit/internal/services/ske/ske_acc_test.go b/stackit/internal/services/ske/ske_acc_test.go index 2776c133b..5e8c81a30 100644 --- a/stackit/internal/services/ske/ske_acc_test.go +++ b/stackit/internal/services/ske/ske_acc_test.go @@ -10,8 +10,12 @@ import ( "github.com/hashicorp/terraform-plugin-testing/config" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" "github.com/stackitcloud/stackit-sdk-go/core/utils" ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api/wait" @@ -119,13 +123,15 @@ func configVarsMaxUpdated() config.Variables { func TestAccSKEMin(t *testing.T) { resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_10_0), + }, + ProtoV6ProviderFactories: testutil.TestEphemeralAccProtoV6ProviderFactories, CheckDestroy: testAccCheckSKEDestroy, Steps: []resource.TestStep{ - // 1) Creation { - Config: testutil.NewConfigBuilder().BuildProviderConfig() + "\n" + resourceMin, + Config: testutil.NewConfigBuilder().Experiments(testutil.ExperimentSKE).BuildProviderConfig() + "\n" + resourceMin, ConfigVariables: testConfigVarsMin, Check: resource.ComposeAggregateTestCheckFunc( // cluster data @@ -163,10 +169,17 @@ func TestAccSKEMin(t *testing.T) { resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "access.idp.enabled", "false"), resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "access.idp.type", "stackit"), ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "echo.example", + tfjsonpath.New("data"), + knownvalue.NotNull(), + ), + }, }, // 2) Data source { - Config: resourceMin, + Config: testutil.NewConfigBuilder().Experiments(testutil.ExperimentSKE).BuildProviderConfig() + "\n" + resourceMin, ConfigVariables: testConfigVarsMin, Check: resource.ComposeAggregateTestCheckFunc( @@ -219,7 +232,7 @@ func TestAccSKEMin(t *testing.T) { }, // 4) Update kubernetes version, OS version and maintenance end, downgrade of kubernetes version { - Config: resourceMin, + Config: testutil.NewConfigBuilder().Experiments(testutil.ExperimentSKE).BuildProviderConfig() + "\n" + resourceMin, ConfigVariables: configVarsMinUpdated(), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ @@ -266,13 +279,15 @@ func TestAccSKEMin(t *testing.T) { func TestAccSKEMax(t *testing.T) { resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_10_0), + }, + ProtoV6ProviderFactories: testutil.TestEphemeralAccProtoV6ProviderFactories, CheckDestroy: testAccCheckSKEDestroy, Steps: []resource.TestStep{ - // 1) Creation { - Config: testutil.NewConfigBuilder().BuildProviderConfig() + "\n" + resourceMax, + Config: testutil.NewConfigBuilder().Experiments(testutil.ExperimentSKE).BuildProviderConfig() + "\n" + resourceMax, ConfigVariables: testConfigVarsMax, Check: resource.ComposeAggregateTestCheckFunc( // cluster data @@ -347,10 +362,17 @@ func TestAccSKEMax(t *testing.T) { resource.TestCheckResourceAttr("stackit_ske_kubeconfig.kubeconfig", "refresh_before", testutil.ConvertConfigVariable(testConfigVarsMax["refresh_before"])), resource.TestCheckResourceAttrSet("stackit_ske_kubeconfig.kubeconfig", "expires_at"), ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "echo.example", + tfjsonpath.New("data"), + knownvalue.NotNull(), + ), + }, }, // 2) Data source { - Config: resourceMax, + Config: testutil.NewConfigBuilder().Experiments(testutil.ExperimentSKE).BuildProviderConfig() + "\n" + resourceMax, ConfigVariables: testConfigVarsMax, Check: resource.ComposeAggregateTestCheckFunc( @@ -438,7 +460,7 @@ func TestAccSKEMax(t *testing.T) { }, // 4) Update kubernetes version, OS version and maintenance end, downgrade of kubernetes version, set access.idp.enabled to false { - Config: resourceMax, + Config: testutil.NewConfigBuilder().Experiments(testutil.ExperimentSKE).BuildProviderConfig() + "\n" + resourceMax, ConfigVariables: configVarsMaxUpdated(), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ diff --git a/stackit/internal/services/ske/testdata/resource-max.tf b/stackit/internal/services/ske/testdata/resource-max.tf index 92377aa0e..176eca925 100644 --- a/stackit/internal/services/ske/testdata/resource-max.tf +++ b/stackit/internal/services/ske/testdata/resource-max.tf @@ -113,6 +113,7 @@ resource "stackit_ske_kubeconfig" "kubeconfig" { expiration = var.expiration refresh = var.refresh refresh_before = var.refresh_before + region = var.region } data "stackit_ske_cluster" "cluster" { @@ -120,10 +121,23 @@ data "stackit_ske_cluster" "cluster" { name = stackit_ske_cluster.cluster.name } - resource "stackit_dns_zone" "dns-zone" { project_id = var.project_id name = var.dns_zone_name dns_name = var.dns_name } +ephemeral "stackit_ske_kubeconfig" "ephemeral_kubeconfig" { + project_id = var.project_id + # cluster_name is unknown during the plan phase because stackit_ske_cluster.cluster.id is computed. + # This forces Terraform to defer the Open call until the Apply phase, after the cluster is ready. + cluster_name = stackit_ske_cluster.cluster.id != "" ? stackit_ske_cluster.cluster.name : "" + expiration = var.expiration + region = var.region +} + +provider "echo" { + data = ephemeral.stackit_ske_kubeconfig.ephemeral_kubeconfig.kube_config +} + +resource "echo" "example" {} diff --git a/stackit/internal/services/ske/testdata/resource-min.tf b/stackit/internal/services/ske/testdata/resource-min.tf index ab1784e64..32ee914d9 100644 --- a/stackit/internal/services/ske/testdata/resource-min.tf +++ b/stackit/internal/services/ske/testdata/resource-min.tf @@ -54,4 +54,15 @@ data "stackit_ske_cluster" "cluster" { name = stackit_ske_cluster.cluster.name } +ephemeral "stackit_ske_kubeconfig" "ephemeral_kubeconfig" { + project_id = var.project_id + # cluster_name is unknown during the plan phase because stackit_ske_cluster.cluster.id is computed. + # This forces Terraform to defer the Open call until the Apply phase, after the cluster is ready. + cluster_name = stackit_ske_cluster.cluster.id != "" ? stackit_ske_cluster.cluster.name : "" +} + +provider "echo" { + data = ephemeral.stackit_ske_kubeconfig.ephemeral_kubeconfig.kube_config +} +resource "echo" "example" {} diff --git a/stackit/internal/testutil/testutil.go b/stackit/internal/testutil/testutil.go index 3887570cb..07c9e45a4 100644 --- a/stackit/internal/testutil/testutil.go +++ b/stackit/internal/testutil/testutil.go @@ -152,6 +152,7 @@ const ( ExperimentIAM Experiment = "iam" ExperimentDremio Experiment = "dremio" ExperimentVPC Experiment = "vpc" + ExperimentSKE Experiment = "ske" ) type customEndpointConfig struct { diff --git a/stackit/internal/testutil/testutil_test.go b/stackit/internal/testutil/testutil_test.go index d4dd144f3..4dde17d45 100644 --- a/stackit/internal/testutil/testutil_test.go +++ b/stackit/internal/testutil/testutil_test.go @@ -95,11 +95,11 @@ func TestConfigBuilderProviderConfig(t *testing.T) { { name: "experiments", builder: NewConfigBuilder(). - Experiments(ExperimentIAM, ExperimentNetwork), + Experiments(ExperimentIAM, ExperimentNetwork, ExperimentSKE), want: `provider "stackit" { default_region = "eu01" enable_beta_resources = false - experiments = ["iam", "network"] + experiments = ["iam", "network", "ske"] }`, }, { diff --git a/stackit/provider.go b/stackit/provider.go index f99c5eb3c..60f67fab5 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -902,5 +902,6 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource { func (p *Provider) EphemeralResources(_ context.Context) []func() ephemeral.EphemeralResource { return []func() ephemeral.EphemeralResource{ access_token.NewAccessTokenEphemeralResource, + skeKubeconfig.NewKubeconfigEphemeralResource, } }