diff --git a/docs/resources/ske_cluster.md b/docs/resources/ske_cluster.md index 2dcccf85a..f0a701a60 100644 --- a/docs/resources/ske_cluster.md +++ b/docs/resources/ske_cluster.md @@ -193,15 +193,12 @@ Optional: ### Nested Schema for `maintenance` -Required: - -- `end` (String) Time for maintenance window end. E.g. `01:23:45Z`, `05:00:00+02:00`. -- `start` (String) Time for maintenance window start. E.g. `01:23:45Z`, `05:00:00+02:00`. - Optional: - `enable_kubernetes_version_updates` (Boolean) Flag to enable/disable auto-updates of the Kubernetes version. Defaults to `true`. SKE automatically updates the cluster Kubernetes version if you have set `maintenance.enable_kubernetes_version_updates` to true or if there is a mandatory update, as described in [General information for Kubernetes & OS updates](https://docs.stackit.cloud/products/runtime/kubernetes-engine/basics/version-updates/). - `enable_machine_image_version_updates` (Boolean) Flag to enable/disable auto-updates of the OS image version. Defaults to `true`. SKE automatically updates the cluster Kubernetes version if you have set `maintenance.enable_kubernetes_version_updates` to true or if there is a mandatory update, as described in [General information for Kubernetes & OS updates](https://docs.stackit.cloud/products/runtime/kubernetes-engine/basics/version-updates/). +- `end` (String) Time for maintenance window end. E.g. `01:23:45Z`, `05:00:00+02:00`. +- `start` (String) Time for maintenance window start. E.g. `01:23:45Z`, `05:00:00+02:00`. diff --git a/stackit/internal/services/ske/cluster/resource.go b/stackit/internal/services/ske/cluster/resource.go index 60a00245e..e274754fe 100644 --- a/stackit/internal/services/ske/cluster/resource.go +++ b/stackit/internal/services/ske/cluster/resource.go @@ -14,12 +14,15 @@ import ( stringplanmodifierUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils/planmodifiers/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator" "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" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int32default" "github.com/hashicorp/terraform-plugin-framework/resource/schema/listdefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" @@ -524,37 +527,59 @@ func (r *clusterResource) Schema(_ context.Context, _ resource.SchemaRequest, re PlanModifiers: []planmodifier.Object{ objectplanmodifier.UseStateForUnknown(), }, + Validators: []validator.Object{ + objectvalidator.AlsoRequires( + path.MatchRelative().AtName("start"), + path.MatchRelative().AtName("end"), + ), + }, Attributes: map[string]schema.Attribute{ "enable_kubernetes_version_updates": schema.BoolAttribute{ Description: "Flag to enable/disable auto-updates of the Kubernetes version. Defaults to `true`. " + SKEUpdateDoc, Optional: true, Computed: true, Default: booldefault.StaticBool(true), + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + }, }, "enable_machine_image_version_updates": schema.BoolAttribute{ Description: "Flag to enable/disable auto-updates of the OS image version. Defaults to `true`. " + SKEUpdateDoc, Optional: true, Computed: true, Default: booldefault.StaticBool(true), + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + }, }, "start": schema.StringAttribute{ Description: "Time for maintenance window start. E.g. `01:23:45Z`, `05:00:00+02:00`.", - Required: true, + Optional: true, + Computed: true, Validators: []validator.String{ stringvalidator.RegexMatches( regexp.MustCompile(`^(((\d{2}:\d{2}:\d{2}(?:\.\d+)?))(Z|[\+-]\d{2}:\d{2})?)$`), "must be a full-time as defined by RFC3339, Section 5.6. E.g. `01:23:45Z`, `05:00:00+02:00`", ), + stringvalidator.AlsoRequires(path.MatchRelative().AtParent().AtName("end")), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), }, }, "end": schema.StringAttribute{ Description: "Time for maintenance window end. E.g. `01:23:45Z`, `05:00:00+02:00`.", - Required: true, + Optional: true, + Computed: true, Validators: []validator.String{ stringvalidator.RegexMatches( regexp.MustCompile(`^(((\d{2}:\d{2}:\d{2}(?:\.\d+)?))(Z|[\+-]\d{2}:\d{2})?)$`), "must be a full-time as defined by RFC3339, Section 5.6. E.g. `01:23:45Z`, `05:00:00+02:00`", ), + stringvalidator.AlsoRequires(path.MatchRelative().AtParent().AtName("start")), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), }, }, }, @@ -563,6 +588,9 @@ func (r *clusterResource) Schema(_ context.Context, _ resource.SchemaRequest, re Description: "Network block as defined below.", Optional: true, Computed: true, + PlanModifiers: []planmodifier.Object{ + objectplanmodifier.UseStateForUnknown(), + }, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ Description: "ID of the STACKIT Network Area (SNA) network into which the cluster will be deployed.", @@ -572,13 +600,17 @@ func (r *clusterResource) Schema(_ context.Context, _ resource.SchemaRequest, re validate.UUID(), }, PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), + stringplanmodifier.RequiresReplaceIfConfigured(), + stringplanmodifier.UseStateForUnknown(), }, }, "control_plane": schema.SingleNestedAttribute{ Description: "Control plane for the cluster.", Optional: true, Computed: true, + PlanModifiers: []planmodifier.Object{ + objectplanmodifier.UseStateForUnknown(), + }, Attributes: map[string]schema.Attribute{ "access_scope": schema.StringAttribute{ Description: "Access scope of the control plane. It defines if the Kubernetes control plane is public or only available inside a STACKIT Network Area." + utils.FormatPossibleValues(sdkUtils.EnumSliceToStringSlice(ske.AllowedAccessScopeEnumValues)...) + " The field is immutable!", @@ -586,6 +618,7 @@ func (r *clusterResource) Schema(_ context.Context, _ resource.SchemaRequest, re Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), + stringplanmodifier.UseStateForUnknown(), }, }, }, diff --git a/stackit/internal/services/ske/ske_acc_test.go b/stackit/internal/services/ske/ske_acc_test.go index ac567cb57..0a5dffab9 100644 --- a/stackit/internal/services/ske/ske_acc_test.go +++ b/stackit/internal/services/ske/ske_acc_test.go @@ -5,18 +5,17 @@ import ( _ "embed" "fmt" "maps" - "strings" "testing" "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/plancheck" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/ske" "github.com/stackitcloud/stackit-sdk-go/services/ske/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" ) @@ -217,23 +216,42 @@ func TestAccSKEMin(t *testing.T) { { Config: resourceMin, ConfigVariables: configVarsMinUpdated(), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("stackit_ske_cluster.cluster", plancheck.ResourceActionUpdate), + }, + }, Check: resource.ComposeAggregateTestCheckFunc( // cluster data - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.#", "1"), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.availability_zones.#", "1"), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.availability_zones.0", testutil.ConvertConfigVariable(testConfigVarsMin["nodepool_availability_zone1"])), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.machine_type", testutil.ConvertConfigVariable(testConfigVarsMin["nodepool_machine_type"])), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.maximum", testutil.ConvertConfigVariable(testConfigVarsMin["nodepool_maximum"])), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.minimum", testutil.ConvertConfigVariable(testConfigVarsMin["nodepool_minimum"])), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.name", testutil.ConvertConfigVariable(testConfigVarsMin["nodepool_name"])), + resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "project_id", testutil.ConvertConfigVariable(configVarsMinUpdated()["project_id"])), + resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "name", testutil.ConvertConfigVariable(configVarsMinUpdated()["name"])), - resource.TestCheckResourceAttrSet("data.stackit_ske_cluster.cluster", "kubernetes_version_used"), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "maintenance.enable_kubernetes_version_updates", testutil.ConvertConfigVariable(testConfigVarsMax["maintenance_enable_kubernetes_version_updates"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "maintenance.enable_machine_image_version_updates", testutil.ConvertConfigVariable(testConfigVarsMax["maintenance_enable_machine_image_version_updates"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "maintenance.start", testutil.ConvertConfigVariable(testConfigVarsMax["maintenance_start"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "maintenance.end", testutil.ConvertConfigVariable(testConfigVarsMax["maintenance_end"])), - resource.TestCheckResourceAttrSet("data.stackit_ske_cluster.cluster", "kubernetes_version_used"), - resource.TestCheckResourceAttrSet("stackit_ske_cluster.cluster", "region"), + resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.#", "1"), + resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.availability_zones.#", "1"), + resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.availability_zones.0", testutil.ConvertConfigVariable(configVarsMinUpdated()["nodepool_availability_zone1"])), + resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.machine_type", testutil.ConvertConfigVariable(configVarsMinUpdated()["nodepool_machine_type"])), + resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.maximum", testutil.ConvertConfigVariable(configVarsMinUpdated()["nodepool_maximum"])), + resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.minimum", testutil.ConvertConfigVariable(configVarsMinUpdated()["nodepool_minimum"])), + resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.name", testutil.ConvertConfigVariable(configVarsMinUpdated()["nodepool_name"])), + resource.TestCheckResourceAttrSet("stackit_ske_cluster.cluster", "node_pools.0.os_version_used"), + resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "kubernetes_version_min", testutil.ConvertConfigVariable(configVarsMinUpdated()["kubernetes_version_min"])), + resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "maintenance.enable_kubernetes_version_updates", testutil.ConvertConfigVariable(configVarsMinUpdated()["maintenance_enable_kubernetes_version_updates"])), + resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "maintenance.enable_machine_image_version_updates", testutil.ConvertConfigVariable(configVarsMinUpdated()["maintenance_enable_machine_image_version_updates"])), + resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "maintenance.start", testutil.ConvertConfigVariable(configVarsMinUpdated()["maintenance_start"])), + resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "maintenance.end", testutil.ConvertConfigVariable(configVarsMinUpdated()["maintenance_end"])), + resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "region", testutil.ConvertConfigVariable(configVarsMinUpdated()["region"])), + resource.TestCheckResourceAttrSet("stackit_ske_cluster.cluster", "kubernetes_version_used"), + resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "network.control_plane.access_scope", testutil.ConvertConfigVariable(configVarsMinUpdated()["network_control_plane_access_scope"])), + + // Kubeconfig + resource.TestCheckResourceAttrPair( + "stackit_ske_kubeconfig.kubeconfig", "project_id", + "stackit_ske_cluster.cluster", "project_id", + ), + resource.TestCheckResourceAttrPair( + "stackit_ske_kubeconfig.kubeconfig", "cluster_name", + "stackit_ske_cluster.cluster", "name", + ), ), }, // Deletion is done by the framework implicitly @@ -412,6 +430,11 @@ func TestAccSKEMax(t *testing.T) { { Config: resourceMax, ConfigVariables: configVarsMaxUpdated(), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("stackit_ske_cluster.cluster", plancheck.ResourceActionUpdate), + }, + }, Check: resource.ComposeAggregateTestCheckFunc( // cluster data resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "project_id", testutil.ConvertConfigVariable(configVarsMaxUpdated()["project_id"])), @@ -513,8 +536,7 @@ func testAccCheckSKEDestroy(s *terraform.State) error { if rs.Type != "stackit_ske_cluster" { continue } - // cluster terraform ID: = "[project_id],[region],[cluster_name]" - clusterName := strings.Split(rs.Primary.ID, core.Separator)[2] + clusterName := rs.Primary.Attributes["name"] clustersToDestroy = append(clustersToDestroy, clusterName) } diff --git a/stackit/internal/services/ske/ske_test.go b/stackit/internal/services/ske/ske_test.go index 5ab9281fa..ae829a7fd 100644 --- a/stackit/internal/services/ske/ske_test.go +++ b/stackit/internal/services/ske/ske_test.go @@ -5,10 +5,11 @@ import ( "net/http" "regexp" "testing" + "time" "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/hashicorp/terraform-plugin-testing/plancheck" "github.com/stackitcloud/stackit-sdk-go/services/serviceenablement" "github.com/stackitcloud/stackit-sdk-go/services/ske" @@ -59,14 +60,14 @@ resource "stackit_ske_cluster" "cluster" { testutil.MockResponse{ Description: "service enablement request", ToJsonBody: serviceenablement.ServiceStatus{ - State: utils.Ptr(serviceenablement.SERVICESTATUSSTATE_ENABLED), + State: new(serviceenablement.SERVICESTATUSSTATE_ENABLED), }, StatusCode: http.StatusOK, }, testutil.MockResponse{ Description: "service enablement wait handler", ToJsonBody: serviceenablement.ServiceStatus{ - State: utils.Ptr(serviceenablement.SERVICESTATUSSTATE_ENABLED), + State: new(serviceenablement.SERVICESTATUSSTATE_ENABLED), Error: nil, }, StatusCode: http.StatusOK, @@ -84,7 +85,7 @@ resource "stackit_ske_cluster" "cluster" { ExpirationDate: nil, Cri: new([]ske.CRI{ { - Name: utils.Ptr(ske.CRINAME_CONTAINERD), + Name: new(ske.CRINAME_CONTAINERD), }, }), }, @@ -93,14 +94,14 @@ resource "stackit_ske_cluster" "cluster" { }), MachineTypes: new([]ske.MachineType{ { - Name: utils.Ptr(machineType), + Name: new(machineType), }, }), KubernetesVersions: new([]ske.KubernetesVersion{ { State: new("supported"), ExpirationDate: nil, - Version: utils.Ptr(kubernetesVersionMin), + Version: new(kubernetesVersionMin), }, }), }, @@ -108,7 +109,7 @@ resource "stackit_ske_cluster" "cluster" { testutil.MockResponse{ Description: "create", ToJsonBody: ske.Cluster{ - Name: utils.Ptr(string(clusterName)), + Name: new(string(clusterName)), }, }, testutil.MockResponse{ @@ -147,3 +148,208 @@ resource "stackit_ske_cluster" "cluster" { }, }) } + +func TestSKEClusterNetworkEmpty(t *testing.T) { + projectId := uuid.NewString() + const ( + clusterName = "cluster-name" + kubernetesVersionMin = "1.33.8" + region = "eu01" + machineType = "g2i.2" + nodeName = "node-name" + ) + 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 + max_surge = 1 + max_unavailable = 0 + name = "%s" + volume_type = "storage_premium_perf4" + volume_size = 50 + labels = {} + } + ] + network = {} +} + +`, region, s.Server.URL, projectId, clusterName, kubernetesVersionMin, machineType, nodeName) + + skeCluster := ske.Cluster{ + Name: new(clusterName), + Nodepools: new([]ske.Nodepool{ + { + AllowSystemComponents: new(true), + AvailabilityZones: new([]string{"eu01-1"}), + Name: new(nodeName), + Cri: new(ske.CRI{ + Name: new(ske.CRINAME_CONTAINERD), + }), + Machine: new(ske.Machine{ + Image: new(ske.Image{ + Name: new("flatcar"), + Version: new("1.0.0"), + }), + Type: new(machineType), + }), + MaxSurge: new(int64(1)), + MaxUnavailable: new(int64(0)), + Maximum: new(int64(2)), + Minimum: new(int64(1)), + Volume: new(ske.Volume{ + Size: new(int64(50)), + Type: new("storage_premium_perf4"), + }), + Labels: new(map[string]string{}), + }, + }), + Kubernetes: new(ske.Kubernetes{ + Version: new(kubernetesVersionMin), + }), + Network: &ske.Network{ + Id: nil, + ControlPlane: new(ske.V2ControlPlaneNetwork{ + AccessScope: new(ske.ACCESSSCOPE_PUBLIC), + }), + }, + Maintenance: new(ske.Maintenance{ + AutoUpdate: new(ske.MaintenanceAutoUpdate{ + KubernetesVersion: new(true), + MachineImageVersion: new(true), + }), + TimeWindow: new(ske.TimeWindow{ + Start: new(time.Now()), + End: new(time.Now()), + }), + }), + Status: new(ske.ClusterStatus{ + Aggregated: new(ske.CLUSTERSTATUSSTATE_HEALTHY), + PodAddressRanges: new([]string{"100.64.0.0/10"}), + }), + Extensions: new(ske.Extension{}), + } + + 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: new(serviceenablement.SERVICESTATUSSTATE_ENABLED), + }, + StatusCode: http.StatusOK, + }, + testutil.MockResponse{ + Description: "service enablement wait handler", + ToJsonBody: serviceenablement.ServiceStatus{ + State: new(serviceenablement.SERVICESTATUSSTATE_ENABLED), + Error: nil, + }, + StatusCode: http.StatusOK, + }, + testutil.MockResponse{ + Description: "kubernetes versions", + ToJsonBody: ske.ProviderOptions{ + MachineImages: new([]ske.MachineImage{ + { + Name: new("flatcar"), + Versions: new([]ske.MachineImageVersion{ + { + State: new("supported"), + Version: new("1.0.0"), + ExpirationDate: nil, + Cri: new([]ske.CRI{ + { + Name: new(ske.CRINAME_CONTAINERD), + }, + }), + }, + }), + }, + }), + MachineTypes: new([]ske.MachineType{ + { + Name: new(machineType), + }, + }), + KubernetesVersions: new([]ske.KubernetesVersion{ + { + State: new("supported"), + ExpirationDate: nil, + Version: new(kubernetesVersionMin), + }, + }), + }, + }, + testutil.MockResponse{ + Description: "create", + ToJsonBody: skeCluster, + }, + testutil.MockResponse{ + Description: "wait done", + ToJsonBody: skeCluster, + }, + testutil.MockResponse{ + Description: "refresh", + ToJsonBody: skeCluster, + }, + testutil.MockResponse{Description: "delete", StatusCode: http.StatusAccepted}, + testutil.MockResponse{Description: "ListClusterResponse is called for checking removal", + ToJsonBody: ske.ListClustersResponse{ + Items: &[]ske.Cluster{}, + }, + }, + ) + }, + Config: tfConfig, + }, + { + Config: tfConfig, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + // Check that no update or replace will be triggered, if config has not changed + plancheck.ExpectResourceAction("stackit_ske_cluster.cluster", plancheck.ResourceActionNoop), + }, + }, + PreConfig: func() { + s.Reset( + testutil.MockResponse{ + Description: "refresh", + ToJsonBody: skeCluster, + }, + testutil.MockResponse{ + Description: "get", + ToJsonBody: skeCluster, + }, + testutil.MockResponse{Description: "delete", StatusCode: http.StatusAccepted}, + testutil.MockResponse{Description: "ListClusterResponse is called for checking removal", + ToJsonBody: ske.ListClustersResponse{ + Items: &[]ske.Cluster{}, + }, + }, + ) + }, + }, + }, + }) +}