From c463fb0a97e1d81441c49e50fe96f8b2fa0f4136 Mon Sep 17 00:00:00 2001 From: Alexander Dahmen Date: Wed, 11 Mar 2026 13:04:36 +0100 Subject: [PATCH] fix(ske): Store ids immediately after provisioning STACKITTPR-397 Signed-off-by: Alexander Dahmen --- .../internal/services/ske/cluster/resource.go | 22 ++- .../services/ske/kubeconfig/resource.go | 10 +- stackit/internal/services/ske/ske_test.go | 148 ++++++++++++++++++ 3 files changed, 169 insertions(+), 11 deletions(-) create mode 100644 stackit/internal/services/ske/ske_test.go diff --git a/stackit/internal/services/ske/cluster/resource.go b/stackit/internal/services/ske/cluster/resource.go index 456e9bb33..a4bb35216 100644 --- a/stackit/internal/services/ske/cluster/resource.go +++ b/stackit/internal/services/ske/cluster/resource.go @@ -17,7 +17,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" @@ -730,9 +729,16 @@ func (r *clusterResource) Create(ctx context.Context, req resource.CreateRequest projectId := model.ProjectId.ValueString() region := model.Region.ValueString() clusterName := model.Name.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "name", clusterName) - ctx = tflog.SetField(ctx, "region", region) + + // Write id attributes to state before polling via the wait handler - just in case anything goes wrong during the wait handler + ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": projectId, + "region": region, + "name": clusterName, + }) + if resp.Diagnostics.HasError() { + return + } // If SKE functionality is not enabled, enable it err := r.enablementClient.EnableServiceRegional(ctx, region, projectId, utils.SKEServiceId).Execute() @@ -2237,8 +2243,10 @@ func (r *clusterResource) ImportState(ctx context.Context, req resource.ImportSt return } - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), idParts[1])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), idParts[2])...) + ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": idParts[0], + "region": idParts[1], + "name": idParts[2], + }) tflog.Info(ctx, "SKE cluster state imported") } diff --git a/stackit/internal/services/ske/kubeconfig/resource.go b/stackit/internal/services/ske/kubeconfig/resource.go index 0008d5f9d..b738456c1 100644 --- a/stackit/internal/services/ske/kubeconfig/resource.go +++ b/stackit/internal/services/ske/kubeconfig/resource.go @@ -255,10 +255,12 @@ func (r *kubeconfigResource) Create(ctx context.Context, req resource.CreateRequ model.KubeconfigId = types.StringValue(kubeconfigUUID) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "cluster_name", clusterName) - ctx = tflog.SetField(ctx, "kube_config_id", kubeconfigUUID) - ctx = tflog.SetField(ctx, "region", region) + ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": projectId, + "cluster_name": clusterName, + "kube_config_id": kubeconfigUUID, + "region": region, + }) err := r.createKubeconfig(ctx, &model) diff --git a/stackit/internal/services/ske/ske_test.go b/stackit/internal/services/ske/ske_test.go new file mode 100644 index 000000000..f47ecca6c --- /dev/null +++ b/stackit/internal/services/ske/ske_test.go @@ -0,0 +1,148 @@ +package ske + +import ( + "fmt" + "net/http" + "regexp" + "testing" + + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/serviceenablement" + "github.com/stackitcloud/stackit-sdk-go/services/ske" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" +) + +func TestSKEClusterSavesIDsOnError(t *testing.T) { + projectId := uuid.NewString() + const ( + clusterName = "cluster-name" + kubernetesVersionMin = "1.33.8" + region = "eu01" + machineType = "g2i.2" + ) + s := testutil.NewMockServer(t) + defer s.Server.Close() + tfConfig := fmt.Sprintf(` +provider "stackit" { + default_region = "%s" + ske_custom_endpoint = "%[2]s" + service_enablement_custom_endpoint = "%[2]s" + service_account_token = "mock-server-needs-no-auth" +} + +resource "stackit_ske_cluster" "cluster" { + project_id = "%s" + name = "%s" + kubernetes_version_min = "%s" + node_pools = [{ + availability_zones = ["eu01-1"] + machine_type = "%s" + os_version_min = "1.0.0" + maximum = 2 + minimum = 1 + name = "node-name" + } + ] +} + +`, region, s.Server.URL, projectId, clusterName, kubernetesVersionMin, machineType) + + resource.UnitTest(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + PreConfig: func() { + s.Reset( + testutil.MockResponse{ + Description: "service enablement request", + ToJsonBody: serviceenablement.ServiceStatus{ + State: utils.Ptr(serviceenablement.SERVICESTATUSSTATE_ENABLED), + }, + StatusCode: http.StatusOK, + }, + testutil.MockResponse{ + Description: "service enablement wait handler", + ToJsonBody: serviceenablement.ServiceStatus{ + State: utils.Ptr(serviceenablement.SERVICESTATUSSTATE_ENABLED), + Error: nil, + }, + StatusCode: http.StatusOK, + }, + testutil.MockResponse{ + Description: "kubernetes versions", + ToJsonBody: ske.ProviderOptions{ + MachineImages: utils.Ptr([]ske.MachineImage{ + { + Name: utils.Ptr("flatcar"), + Versions: utils.Ptr([]ske.MachineImageVersion{ + { + State: utils.Ptr("supported"), + Version: utils.Ptr("1.0.0"), + ExpirationDate: nil, + Cri: utils.Ptr([]ske.CRI{ + { + Name: utils.Ptr(ske.CRINAME_CONTAINERD), + }, + }), + }, + }), + }, + }), + MachineTypes: utils.Ptr([]ske.MachineType{ + { + Name: utils.Ptr(machineType), + }, + }), + KubernetesVersions: utils.Ptr([]ske.KubernetesVersion{ + { + State: utils.Ptr("supported"), + ExpirationDate: nil, + Version: utils.Ptr(kubernetesVersionMin), + }, + }), + }, + }, + testutil.MockResponse{ + Description: "create", + ToJsonBody: ske.Cluster{ + Name: utils.Ptr(string(clusterName)), + }, + }, + testutil.MockResponse{ + Description: "failing waiter", + StatusCode: http.StatusInternalServerError, + }, + ) + }, + Config: tfConfig, + ExpectError: regexp.MustCompile("Error creating/updating cluster.*"), + }, + { + PreConfig: func() { + s.Reset( + testutil.MockResponse{ + Description: "refresh", + Handler: func(w http.ResponseWriter, req *http.Request) { + expected := fmt.Sprintf("/v2/projects/%s/regions/%s/clusters/%s", projectId, region, clusterName) + if req.URL.Path != expected { + t.Errorf("expected request to %s, got %s", expected, req.URL.Path) + } + w.WriteHeader(http.StatusInternalServerError) + }, + }, + testutil.MockResponse{Description: "delete", StatusCode: http.StatusAccepted}, + testutil.MockResponse{Description: "ListClusterResponse is called for checking removal", + ToJsonBody: ske.ListClustersResponse{ + Items: &[]ske.Cluster{}, + }, + }, + ) + }, + RefreshState: true, + ExpectError: regexp.MustCompile("Error reading cluster*"), + }, + }, + }) +}