From d0c4a5c5d5d96741f4479ad4edac151f43a37f10 Mon Sep 17 00:00:00 2001 From: Mauritz Uphoff Date: Fri, 8 May 2026 10:18:40 +0200 Subject: [PATCH] feat(ske): add ephemeral ske kubeconfig Signed-off-by: Mauritz Uphoff --- docs/ephemeral-resources/ske_kubeconfig.md | 55 +++++ .../ephemeral-resource.tf | 18 ++ .../access_token/access_token_acc_test.go | 3 +- .../ske/kubeconfig/ephemeral_resource.go | 194 ++++++++++++++++++ .../ske/kubeconfig/ephemeral_resource_test.go | 98 +++++++++ stackit/internal/services/ske/ske_acc_test.go | 44 +++- .../services/ske/testdata/resource-max.tf | 15 ++ .../services/ske/testdata/resource-min.tf | 12 ++ stackit/provider.go | 1 + 9 files changed, 428 insertions(+), 12 deletions(-) create mode 100644 docs/ephemeral-resources/ske_kubeconfig.md create mode 100644 examples/ephemeral-resources/stackit_ske_kubeconfig/ephemeral-resource.tf create mode 100644 stackit/internal/services/ske/kubeconfig/ephemeral_resource.go create mode 100644 stackit/internal/services/ske/kubeconfig/ephemeral_resource_test.go diff --git a/docs/ephemeral-resources/ske_kubeconfig.md b/docs/ephemeral-resources/ske_kubeconfig.md new file mode 100644 index 000000000..f39020831 --- /dev/null +++ b/docs/ephemeral-resources/ske_kubeconfig.md @@ -0,0 +1,55 @@ +--- +# 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 in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources. +--- + +# stackit_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 in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources. + +## Example Usage + +```terraform +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. Defaults to `1800` (30m). Maximum is `14400` (4h). +- `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/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..c7b531d53 --- /dev/null +++ b/examples/ephemeral-resources/stackit_ske_kubeconfig/ephemeral-resource.tf @@ -0,0 +1,18 @@ +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/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..070ab0550 --- /dev/null +++ b/stackit/internal/services/ske/kubeconfig/ephemeral_resource.go @@ -0,0 +1,194 @@ +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" +) + +const ( + defaultKubeconfigExpiration = 1800 +) + +// 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 + } + + features.CheckBetaResourcesEnabled( + ctx, + &ephemeralProviderData.ProviderData, + &resp.Diagnostics, + "stackit_ske_kubeconfig", "ephemeral_resource", + ) + if resp.Diagnostics.HasError() { + return + } + + e.providerData = ephemeralProviderData.ProviderData + 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.AddBetaDescription( + "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.", + 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. Defaults to `1800` (30m). Maximum is `14400` (4h).", + Optional: true, + Validators: []validator.Int64{ + int64validator.AtLeast(60), + 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) + + // Kubeconfig only needs to be valid for the duration of the Terraform operation. + // Defaulted to 1800s (30m) for better security than the API default (3600s). + expiration := conversion.Int64ValueToPointer(model.Expiration) + if expiration == nil { + expiration = new(int64) + *expiration = defaultKubeconfigExpiration + } + + kubeconfigResp, err := getKubeconfig(ctx, e.client, projectId, region, clusterName, 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.APIClient, projectId, region, clusterName string, expiration *int64) (*ske.Kubeconfig, error) { + var expirationStringPtr *string + if expiration != nil { + expirationStringPtr = new(string) + *expirationStringPtr = strconv.FormatInt(*expiration, 10) + } + + payload := ske.CreateKubeconfigPayload{ + ExpirationSeconds: expirationStringPtr, + } + + return client.DefaultAPI.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..1f8974b64 --- /dev/null +++ b/stackit/internal/services/ske/kubeconfig/ephemeral_resource_test.go @@ -0,0 +1,98 @@ +package ske + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/stackitcloud/stackit-sdk-go/core/config" + 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 + mockStatusCode int + expectError bool + }{ + { + description: "success", + expiration: nil, + mockResponse: &ske.Kubeconfig{ + Kubeconfig: &[]string{kubeconfig}[0], + ExpirationTimestamp: &expirationTime, + AdditionalProperties: make(map[string]any), + }, + mockStatusCode: http.StatusOK, + expectError: false, + }, + { + description: "success with expiration", + expiration: &[]int64{3600}[0], + mockResponse: &ske.Kubeconfig{ + Kubeconfig: &[]string{kubeconfig}[0], + ExpirationTimestamp: &expirationTime, + AdditionalProperties: make(map[string]any), + }, + mockStatusCode: http.StatusOK, + expectError: false, + }, + { + description: "api error", + mockStatusCode: http.StatusInternalServerError, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + expectedPath := fmt.Sprintf("/v2/projects/%s/regions/%s/clusters/%s/kubeconfig", projectId, region, clusterName) + if r.URL.Path != expectedPath { + t.Errorf("Expected path %s, got %s", expectedPath, r.URL.Path) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(tt.mockStatusCode) + if tt.mockResponse != nil { + _ = json.NewEncoder(w).Encode(tt.mockResponse) + } + })) + defer server.Close() + + cfg, err := ske.NewAPIClient( + config.WithEndpoint(server.URL), + config.WithoutAuthentication(), + ) + if err != nil { + t.Fatalf("Failed to create SKE client: %v", err) + } + + resp, err := getKubeconfig(context.Background(), cfg, 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 4a9fb5243..78d250bec 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" @@ -117,13 +121,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().EnableBetaResources(true).BuildProviderConfig() + "\n" + resourceMin, ConfigVariables: testConfigVarsMin, Check: resource.ComposeAggregateTestCheckFunc( // cluster data @@ -157,10 +163,17 @@ func TestAccSKEMin(t *testing.T) { "stackit_ske_cluster.cluster", "name", ), ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "echo.example", + tfjsonpath.New("data"), + knownvalue.NotNull(), + ), + }, }, // 2) Data source { - Config: resourceMin, + Config: testutil.NewConfigBuilder().EnableBetaResources(true).BuildProviderConfig() + "\n" + resourceMin, ConfigVariables: testConfigVarsMin, Check: resource.ComposeAggregateTestCheckFunc( @@ -214,7 +227,7 @@ func TestAccSKEMin(t *testing.T) { }, // 4) Update kubernetes version, OS version and maintenance end, downgrade of kubernetes version { - Config: resourceMin, + Config: testutil.NewConfigBuilder().EnableBetaResources(true).BuildProviderConfig() + "\n" + resourceMin, ConfigVariables: configVarsMinUpdated(), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ @@ -261,13 +274,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().EnableBetaResources(true).BuildProviderConfig() + "\n" + resourceMax, ConfigVariables: testConfigVarsMax, Check: resource.ComposeAggregateTestCheckFunc( // cluster data @@ -338,10 +353,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().EnableBetaResources(true).BuildProviderConfig() + "\n" + resourceMax, ConfigVariables: testConfigVarsMax, Check: resource.ComposeAggregateTestCheckFunc( @@ -428,7 +450,7 @@ func TestAccSKEMax(t *testing.T) { }, // 4) Update kubernetes version, OS version and maintenance end, downgrade of kubernetes version { - Config: resourceMax, + Config: testutil.NewConfigBuilder().EnableBetaResources(true).BuildProviderConfig() + "\n" + resourceMax, ConfigVariables: configVarsMaxUpdated(), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ @@ -501,7 +523,7 @@ func TestAccProviderOption(t *testing.T) { Steps: []resource.TestStep{ { ConfigVariables: testConfigDatasource, - Config: testutil.NewConfigBuilder().BuildProviderConfig() + "\n" + dataSourceProviderOptions, + Config: testutil.NewConfigBuilder().EnableBetaResources(true).BuildProviderConfig() + "\n" + dataSourceProviderOptions, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.stackit_ske_kubernetes_versions.example", "version_state", "SUPPORTED"), resource.TestCheckResourceAttrSet("data.stackit_ske_kubernetes_versions.example", "kubernetes_versions.0.version"), diff --git a/stackit/internal/services/ske/testdata/resource-max.tf b/stackit/internal/services/ske/testdata/resource-max.tf index fde7ff1cc..b59a9b98d 100644 --- a/stackit/internal/services/ske/testdata/resource-max.tf +++ b/stackit/internal/services/ske/testdata/resource-max.tf @@ -106,6 +106,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" { @@ -119,4 +120,18 @@ resource "stackit_dns_zone" "dns-zone" { 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..d8e3a3451 100644 --- a/stackit/internal/services/ske/testdata/resource-min.tf +++ b/stackit/internal/services/ske/testdata/resource-min.tf @@ -54,4 +54,16 @@ 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/provider.go b/stackit/provider.go index 01b56db9c..879c01adc 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -804,5 +804,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, } }