diff --git a/docs/data-sources/ske_cluster.md b/docs/data-sources/ske_cluster.md index 74527f16..64bde65e 100644 --- a/docs/data-sources/ske_cluster.md +++ b/docs/data-sources/ske_cluster.md @@ -36,7 +36,8 @@ This should be used with care since it also disables a couple of other features - `hibernations` (Attributes List) One or more hibernation block as defined below. (see [below for nested schema](#nestedatt--hibernations)) - `id` (String) Terraform's internal data source. ID. It is structured as "`project_id`,`name`". - `kube_config` (String, Sensitive, Deprecated) Kube config file used for connecting to the cluster. This field will be empty for clusters with Kubernetes v1.27+, or if you have obtained the kubeconfig or performed credentials rotation using the new process, either through the Portal or the SKE API. Use the stackit_ske_kubeconfig resource instead. For more information, see How to rotate SKE credentials (https://docs.stackit.cloud/stackit/en/how-to-rotate-ske-credentials-200016334.html). -- `kubernetes_version` (String) Kubernetes version. +- `kubernetes_version` (String, Deprecated) Kubernetes version. This field is deprecated, use `kubernetes_version_used` instead +- `kubernetes_version_min` (String) The minimum Kubernetes version, this field is always nil. 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 [Updates for Kubernetes versions and Operating System versions in SKE](https://docs.stackit.cloud/stackit/en/version-updates-in-ske-10125631.html). To get the current kubernetes version being used for your cluster, use the `kubernetes_version_used` field. - `kubernetes_version_used` (String) Full Kubernetes version used. For example, if `1.22` was selected, this value may result to `1.22.15` - `maintenance` (Attributes) A single maintenance block as defined below (see [below for nested schema](#nestedatt--maintenance)) - `node_pools` (Attributes List) One or more `node_pool` block as defined below. (see [below for nested schema](#nestedatt--node_pools)) diff --git a/docs/resources/ske_cluster.md b/docs/resources/ske_cluster.md index fc0b60ba..66a5fd38 100644 --- a/docs/resources/ske_cluster.md +++ b/docs/resources/ske_cluster.md @@ -41,7 +41,6 @@ resource "stackit_ske_cluster" "example" { ### Required -- `kubernetes_version` (String) Kubernetes version. Must only contain major and minor version (e.g. 1.22) - `name` (String) The cluster name. - `node_pools` (Attributes List) One or more `node_pool` block as defined below. (see [below for nested schema](#nestedatt--node_pools)) - `project_id` (String) STACKIT project ID to which the cluster is associated. @@ -53,13 +52,15 @@ This should be used with care since it also disables a couple of other features Deprecated as of Kubernetes 1.25 and later - `extensions` (Attributes) A single extensions block as defined below. (see [below for nested schema](#nestedatt--extensions)) - `hibernations` (Attributes List) One or more hibernation block as defined below. (see [below for nested schema](#nestedatt--hibernations)) +- `kubernetes_version` (String, Deprecated) Kubernetes version. Must only contain major and minor version (e.g. 1.22). This field is deprecated, use `kubernetes_version_min instead` +- `kubernetes_version_min` (String) The minimum Kubernetes version. This field will be used to set the kubernetes version on creation/update of the cluster and can only by incremented. A downgrade of the version requires a replace of the cluster. If unset, the latest supported Kubernetes version will be used. 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 [Updates for Kubernetes versions and Operating System versions in SKE](https://docs.stackit.cloud/stackit/en/version-updates-in-ske-10125631.html). To get the current kubernetes version being used for your cluster, use the read-only `kubernetes_version_used` field. - `maintenance` (Attributes) A single maintenance block as defined below. (see [below for nested schema](#nestedatt--maintenance)) ### Read-Only - `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`name`". -- `kube_config` (String, Sensitive, Deprecated) Static token kubeconfig used for connecting to the cluster. This field will be empty for clusters with Kubernetes v1.27+, or if you have obtained the kubeconfig or performed credentials rotation using the new process, either through the Portal or the SKE API. Use the stackit_ske_kubeconfig resource instead. For more information, see How to rotate SKE credentials (https://docs.stackit.cloud/stackit/en/how-to-rotate-ske-credentials-200016334.html). -- `kubernetes_version_used` (String) Full Kubernetes version used. For example, if 1.22 was selected, this value may result to 1.22.15 +- `kube_config` (String, Sensitive, Deprecated) Static token kubeconfig used for connecting to the cluster. This field will be empty for clusters with Kubernetes v1.27+, or if you have obtained the kubeconfig or performed credentials rotation using the new process, either through the Portal or the SKE API. Use the stackit_ske_kubeconfig resource instead. For more information, see [How to rotate SKE credentials](https://docs.stackit.cloud/stackit/en/how-to-rotate-ske-credentials-200016334.html). +- `kubernetes_version_used` (String) Full Kubernetes version used. For example, if 1.22 was set in `kubernetes_version_min`, this value may result to 1.22.15. 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 [Updates for Kubernetes versions and Operating System versions in SKE](https://docs.stackit.cloud/stackit/en/version-updates-in-ske-10125631.html). ### Nested Schema for `node_pools` @@ -149,7 +150,10 @@ Optional: Required: -- `enable_kubernetes_version_updates` (Boolean) Flag to enable/disable auto-updates of the Kubernetes version. - `enable_machine_image_version_updates` (Boolean) Flag to enable/disable auto-updates of the OS image version. - `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 [Updates for Kubernetes versions and Operating System versions in SKE](https://docs.stackit.cloud/stackit/en/version-updates-in-ske-10125631.html). diff --git a/stackit/internal/services/ske/cluster/datasource.go b/stackit/internal/services/ske/cluster/datasource.go index 795b814a..8d20c397 100644 --- a/stackit/internal/services/ske/cluster/datasource.go +++ b/stackit/internal/services/ske/cluster/datasource.go @@ -94,10 +94,15 @@ func (r *clusterDataSource) Schema(_ context.Context, _ datasource.SchemaRequest Description: "The cluster name.", Required: true, }, - "kubernetes_version": schema.StringAttribute{ - Description: "Kubernetes version.", + "kubernetes_version_min": schema.StringAttribute{ + Description: `The minimum Kubernetes version, this field is always nil. ` + SKEUpdateDoc + " To get the current kubernetes version being used for your cluster, use the `kubernetes_version_used` field.", Computed: true, }, + "kubernetes_version": schema.StringAttribute{ + Description: "Kubernetes version. This field is deprecated, use `kubernetes_version_used` instead", + Computed: true, + DeprecationMessage: "This field is always nil, use `kubernetes_version_used` to get the cluster kubernetes version. This field would cause errors when the cluster got a kubernetes version minor upgrade, either triggered by automatic or forceful updates.", + }, "kubernetes_version_used": schema.StringAttribute{ Description: "Full Kubernetes version used. For example, if `1.22` was selected, this value may result to `1.22.15`", Computed: true, diff --git a/stackit/internal/services/ske/cluster/resource.go b/stackit/internal/services/ske/cluster/resource.go index 2a5330f2..8e84ba20 100644 --- a/stackit/internal/services/ske/cluster/resource.go +++ b/stackit/internal/services/ske/cluster/resource.go @@ -10,12 +10,14 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" "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/int64default" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier" @@ -46,6 +48,8 @@ const ( VersionStateSupported = "supported" VersionStatePreview = "preview" VersionStateDeprecated = "deprecated" + + SKEUpdateDoc = "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 [Updates for Kubernetes versions and Operating System versions in SKE](https://docs.stackit.cloud/stackit/en/version-updates-in-ske-10125631.html)." ) // Ensure the implementation satisfies the expected interfaces. @@ -59,6 +63,7 @@ type Model struct { Id types.String `tfsdk:"id"` // needed by TF ProjectId types.String `tfsdk:"project_id"` Name types.String `tfsdk:"name"` + KubernetesVersionMin types.String `tfsdk:"kubernetes_version_min"` KubernetesVersion types.String `tfsdk:"kubernetes_version"` KubernetesVersionUsed types.String `tfsdk:"kubernetes_version_used"` AllowPrivilegedContainers types.Bool `tfsdk:"allow_privileged_containers"` @@ -262,13 +267,35 @@ func (r *clusterResource) Schema(_ context.Context, _ resource.SchemaRequest, re "name": schema.StringAttribute{ Description: "The cluster name.", Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, Validators: []validator.String{ validate.NoSeparator(), }, }, + "kubernetes_version_min": schema.StringAttribute{ + Description: "The minimum Kubernetes version. This field will be used to set the kubernetes version on creation/update of the cluster and can only by incremented. A downgrade of the version requires a replace of the cluster. If unset, the latest supported Kubernetes version will be used. " + SKEUpdateDoc + " To get the current kubernetes version being used for your cluster, use the read-only `kubernetes_version_used` field.", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplaceIf(stringplanmodifier.RequiresReplaceIfFunc(func(ctx context.Context, sr planmodifier.StringRequest, rrifr *stringplanmodifier.RequiresReplaceIfFuncResponse) { + if sr.StateValue.IsNull() || sr.PlanValue.IsNull() { + return + } + planVersion := fmt.Sprintf("v%s", sr.PlanValue.ValueString()) + stateVersion := fmt.Sprintf("v%s", sr.StateValue.ValueString()) + + rrifr.RequiresReplace = semver.Compare(planVersion, stateVersion) < 0 + }), "Kubernetes minimum version", "If the Kubernetes version is a downgrade, the cluster will be replaced"), + }, + Validators: []validator.String{ + validate.VersionNumber(), + }, + }, "kubernetes_version": schema.StringAttribute{ - Description: "Kubernetes version. Must only contain major and minor version (e.g. 1.22)", - Required: true, + Description: "Kubernetes version. Must only contain major and minor version (e.g. 1.22). This field is deprecated, use `kubernetes_version_min instead`", + Optional: true, + DeprecationMessage: "Use `kubernetes_version_min instead`. Setting a specific kubernetes version would cause errors when the cluster got a kubernetes version minor upgrade, either triggered by automatic or forceful updates. In those cases, this field might not represent the actual kubernetes version used in the cluster.", PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplaceIf(stringplanmodifier.RequiresReplaceIfFunc(func(ctx context.Context, sr planmodifier.StringRequest, rrifr *stringplanmodifier.RequiresReplaceIfFuncResponse) { if sr.StateValue.IsNull() || sr.PlanValue.IsNull() { @@ -285,7 +312,7 @@ func (r *clusterResource) Schema(_ context.Context, _ resource.SchemaRequest, re }, }, "kubernetes_version_used": schema.StringAttribute{ - Description: "Full Kubernetes version used. For example, if 1.22 was selected, this value may result to 1.22.15", + Description: "Full Kubernetes version used. For example, if 1.22 was set in `kubernetes_version_min`, this value may result to 1.22.15. " + SKEUpdateDoc, Computed: true, }, "allow_privileged_containers": schema.BoolAttribute{ @@ -422,8 +449,10 @@ func (r *clusterResource) Schema(_ context.Context, _ resource.SchemaRequest, re }, Attributes: map[string]schema.Attribute{ "enable_kubernetes_version_updates": schema.BoolAttribute{ - Description: "Flag to enable/disable auto-updates of the Kubernetes version.", - Required: true, + Description: "Flag to enable/disable auto-updates of the Kubernetes version. Defaults to `true. " + SKEUpdateDoc, + Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), }, "enable_machine_image_version_updates": schema.BoolAttribute{ Description: "Flag to enable/disable auto-updates of the OS image version.", @@ -510,25 +539,40 @@ func (r *clusterResource) Schema(_ context.Context, _ resource.SchemaRequest, re }, }, "kube_config": schema.StringAttribute{ - Description: "Static token kubeconfig used for connecting to the cluster. This field will be empty for clusters with Kubernetes v1.27+, or if you have obtained the kubeconfig or performed credentials rotation using the new process, either through the Portal or the SKE API. Use the stackit_ske_kubeconfig resource instead. For more information, see How to rotate SKE credentials (https://docs.stackit.cloud/stackit/en/how-to-rotate-ske-credentials-200016334.html).", + Description: "Static token kubeconfig used for connecting to the cluster. This field will be empty for clusters with Kubernetes v1.27+, or if you have obtained the kubeconfig or performed credentials rotation using the new process, either through the Portal or the SKE API. Use the stackit_ske_kubeconfig resource instead. For more information, see [How to rotate SKE credentials](https://docs.stackit.cloud/stackit/en/how-to-rotate-ske-credentials-200016334.html).", Sensitive: true, Computed: true, - DeprecationMessage: "This field will be empty for clusters with Kubernetes v1.27+, or if you have obtained the kubeconfig or performed credentials rotation using the new process, either through the Portal or the SKE API. Use the stackit_ske_kubeconfig resource instead. For more information, see How to rotate SKE credentials (https://docs.stackit.cloud/stackit/en/how-to-rotate-ske-credentials-200016334.html).", + DeprecationMessage: "This field will be empty for clusters with Kubernetes v1.27+, or if you have obtained the kubeconfig or performed credentials rotation using the new process, either through the Portal or the SKE API. Use the stackit_ske_kubeconfig resource instead. For more information, see [How to rotate SKE credentials](https://docs.stackit.cloud/stackit/en/how-to-rotate-ske-credentials-200016334.html).", }, }, } } +// ConfigValidators validate the resource configuration +func (r *clusterResource) ConfigValidators(_ context.Context) []resource.ConfigValidator { + return []resource.ConfigValidator{ + // will raise an error if both fields are set simultaneously + resourcevalidator.Conflicting( + path.MatchRoot("kubernetes_version"), + path.MatchRoot("kubernetes_version_min"), + ), + } +} + // needs to be executed inside the Create and Update methods // since ValidateConfig runs before variables are rendered to their value, // which causes errors like this: https://github.com/stackitcloud/terraform-provider-stackit/issues/201 func checkAllowPrivilegedContainers(allowPrivilegeContainers types.Bool, kubernetesVersion types.String) diag.Diagnostics { var diags diag.Diagnostics + // if kubernetesVersion is null, the latest one will be used and allowPriviledgeContainers will not be supported if kubernetesVersion.IsNull() { - diags.AddError("'Kubernetes version' missing", "This field is required") + if !allowPrivilegeContainers.IsNull() { + diags.AddError("'Allow privilege containers' deprecated", "This field is deprecated as of Kubernetes 1.25 and later. Please remove this field") + } return diags } + comparison := semver.Compare(fmt.Sprintf("v%s", kubernetesVersion.ValueString()), "v1.25") if comparison < 0 { if allowPrivilegeContainers.IsNull() { @@ -552,7 +596,12 @@ func (r *clusterResource) Create(ctx context.Context, req resource.CreateRequest return } - diags = checkAllowPrivilegedContainers(model.AllowPrivilegedContainers, model.KubernetesVersion) + kubernetesVersion := model.KubernetesVersionMin + // needed for backwards compatibility following kubernetes_version field deprecation + if kubernetesVersion.IsNull() { + kubernetesVersion = model.KubernetesVersion + } + diags = checkAllowPrivilegedContainers(model.AllowPrivilegedContainers, kubernetesVersion) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return @@ -939,12 +988,6 @@ func mapFields(ctx context.Context, cl *ske.Cluster, m *Model) error { ) if cl.Kubernetes != nil { - // The k8s version returned by the API includes the patch version, while we only support major and minor in the kubernetes_version field - // This prevents inconsistent state by automatic updates to the patch version in the API - versionPreffixed := "v" + *cl.Kubernetes.Version - majorMinorVersionPreffixed := semver.MajorMinor(versionPreffixed) - majorMinorVersion, _ := strings.CutPrefix(majorMinorVersionPreffixed, "v") - m.KubernetesVersion = types.StringPointerValue(utils.Ptr(majorMinorVersion)) m.KubernetesVersionUsed = types.StringPointerValue(cl.Kubernetes.Version) m.AllowPrivilegedContainers = types.BoolPointerValue(cl.Kubernetes.AllowPrivilegedContainers) } @@ -1331,7 +1374,7 @@ func mapExtensions(ctx context.Context, cl *ske.Cluster, m *Model) error { } func toKubernetesPayload(m *Model, availableVersions []ske.KubernetesVersion) (kubernetesPayload *ske.Kubernetes, hasDeprecatedVersion bool, err error) { - versionUsed, hasDeprecatedVersion, err := latestMatchingVersion(availableVersions, conversion.StringValueToPointer(m.KubernetesVersion)) + versionUsed, hasDeprecatedVersion, err := latestMatchingVersion(availableVersions, conversion.StringValueToPointer(m.KubernetesVersion), conversion.StringValueToPointer(m.KubernetesVersionMin)) if err != nil { return nil, false, fmt.Errorf("getting latest matching kubernetes version: %w", err) } @@ -1343,41 +1386,74 @@ func toKubernetesPayload(m *Model, availableVersions []ske.KubernetesVersion) (k return k, hasDeprecatedVersion, nil } -func latestMatchingVersion(availableVersions []ske.KubernetesVersion, providedVersion *string) (version *string, deprecated bool, err error) { +func latestMatchingVersion(availableVersions []ske.KubernetesVersion, providedVersion, providedVersionMin *string) (version *string, deprecated bool, err error) { deprecated = false if availableVersions == nil { return nil, false, fmt.Errorf("nil available kubernetes versions") } - if providedVersion == nil { - return nil, false, fmt.Errorf("provided version is nil") + if providedVersionMin == nil { + if providedVersion == nil { + // kubernetes_version field deprecation + // this if clause should be removed once kubernetes_version field is completely removed + latestVersion, err := getLatestSupportedKubernetesVersion(availableVersions) + if err != nil { + return nil, false, fmt.Errorf("get latest supported kubernetes version: %w", err) + } + return latestVersion, false, nil + } + // kubernetes_version field deprecation + // kubernetes_version field value is assigned to kubernets_version_min for backwards compatibility + providedVersionMin = providedVersion } - providedVersionPrefixed := "v" + *providedVersion + var fullVersion bool + versionExp := validate.FullVersionRegex + versionRegex := regexp.MustCompile(versionExp) + if versionRegex.MatchString(*providedVersionMin) { + fullVersion = true + } + + providedVersionPrefixed := "v" + *providedVersionMin if !semver.IsValid(providedVersionPrefixed) { return nil, false, fmt.Errorf("provided version is invalid") } var versionUsed *string + var state *string var availableVersionsArray []string - // Get the higher available version that matches the major and minor version provided by the user + // Get the higher available version that matches the major, minor and patch version provided by the user for _, v := range availableVersions { if v.State == nil || v.Version == nil { continue } availableVersionsArray = append(availableVersionsArray, *v.Version) vPreffixed := "v" + *v.Version - if semver.MajorMinor(vPreffixed) == semver.MajorMinor(providedVersionPrefixed) && - (semver.Compare(vPreffixed, providedVersionPrefixed) == 1 || semver.Compare(vPreffixed, providedVersionPrefixed) == 0) { - versionUsed = v.Version - if strings.EqualFold(*v.State, VersionStateDeprecated) { - deprecated = true - } else { - deprecated = false + if fullVersion { + // [MAJOR].[MINOR].[PATCH] version provided, match available version + if semver.Compare(vPreffixed, providedVersionPrefixed) == 0 { + versionUsed = v.Version + state = v.State + break } + } else { + // [MAJOR].[MINOR] version provided, get the latest patch version + if semver.MajorMinor(vPreffixed) == semver.MajorMinor(providedVersionPrefixed) && + (semver.Compare(vPreffixed, providedVersionPrefixed) == 1) || (semver.Compare(vPreffixed, providedVersionPrefixed) == 0) { + versionUsed = v.Version + state = v.State + } + } + } + + if versionUsed != nil { + if strings.EqualFold(*state, VersionStateDeprecated) { + deprecated = true + } else { + deprecated = false } } @@ -1389,6 +1465,31 @@ func latestMatchingVersion(availableVersions []ske.KubernetesVersion, providedVe return versionUsed, deprecated, nil } +func getLatestSupportedKubernetesVersion(versions []ske.KubernetesVersion) (*string, error) { + foundKubernetesVersion := false + var latestVersion *string + for i := range versions { + version := versions[i] + if *version.State != VersionStateSupported { + continue + } + if latestVersion != nil { + oldSemVer := fmt.Sprintf("v%s", *latestVersion) + newSemVer := fmt.Sprintf("v%s", *version.Version) + if semver.Compare(newSemVer, oldSemVer) != 1 { + continue + } + } + + foundKubernetesVersion = true + latestVersion = version.Version + } + if !foundKubernetesVersion { + return nil, fmt.Errorf("no supported Kubernetes version found") + } + return latestVersion, nil +} + func (r *clusterResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform var state Model diags := req.State.Get(ctx, &state) @@ -1441,7 +1542,13 @@ func (r *clusterResource) Update(ctx context.Context, req resource.UpdateRequest return } - diags = checkAllowPrivilegedContainers(model.AllowPrivilegedContainers, model.KubernetesVersion) + kubernetesVersion := model.KubernetesVersionMin + // needed for backwards compatibility following kubernetes_version field deprecation + if kubernetesVersion.IsNull() { + kubernetesVersion = model.KubernetesVersion + } + + diags = checkAllowPrivilegedContainers(model.AllowPrivilegedContainers, kubernetesVersion) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return diff --git a/stackit/internal/services/ske/cluster/resource_test.go b/stackit/internal/services/ske/cluster/resource_test.go index aad4f714..07f31c94 100644 --- a/stackit/internal/services/ske/cluster/resource_test.go +++ b/stackit/internal/services/ske/cluster/resource_test.go @@ -121,7 +121,7 @@ func TestMapFields(t *testing.T) { Id: types.StringValue("pid,name"), ProjectId: types.StringValue("pid"), Name: types.StringValue("name"), - KubernetesVersion: types.StringValue("1.2"), + KubernetesVersion: types.StringNull(), KubernetesVersionUsed: types.StringValue("1.2.3"), AllowPrivilegedContainers: types.BoolValue(true), @@ -397,6 +397,7 @@ func TestLatestMatchingVersion(t *testing.T) { description string availableVersions []ske.KubernetesVersion providedVersion *string + providedVersionMin *string expectedVersionUsed *string expectedHasDeprecatedVersion bool isValid bool @@ -421,13 +422,66 @@ func TestLatestMatchingVersion(t *testing.T) { State: utils.Ptr(VersionStateSupported), }, }, + nil, + utils.Ptr("1.20.1"), + utils.Ptr("1.20.1"), + false, + true, + }, + { + "available_version_zero_patch", + []ske.KubernetesVersion{ + { + Version: utils.Ptr("1.20.0"), + State: utils.Ptr(VersionStateSupported), + }, + { + Version: utils.Ptr("1.20.1"), + State: utils.Ptr(VersionStateSupported), + }, + { + Version: utils.Ptr("1.20.2"), + State: utils.Ptr(VersionStateSupported), + }, + { + Version: utils.Ptr("1.19.0"), + State: utils.Ptr(VersionStateSupported), + }, + }, + nil, + utils.Ptr("1.20.0"), + utils.Ptr("1.20.0"), + false, + true, + }, + { + "available_version_with_no_provided_patch", + []ske.KubernetesVersion{ + { + Version: utils.Ptr("1.20.0"), + State: utils.Ptr(VersionStateSupported), + }, + { + Version: utils.Ptr("1.20.1"), + State: utils.Ptr(VersionStateSupported), + }, + { + Version: utils.Ptr("1.20.2"), + State: utils.Ptr(VersionStateSupported), + }, + { + Version: utils.Ptr("1.19.0"), + State: utils.Ptr(VersionStateSupported), + }, + }, + nil, utils.Ptr("1.20"), utils.Ptr("1.20.2"), false, true, }, { - "available_version_no_patch", + "available_version_no_provided_patch_2", []ske.KubernetesVersion{ { Version: utils.Ptr("1.20.0"), @@ -438,6 +492,7 @@ func TestLatestMatchingVersion(t *testing.T) { State: utils.Ptr(VersionStateSupported), }, }, + nil, utils.Ptr("1.20"), utils.Ptr("1.20.0"), false, @@ -455,6 +510,7 @@ func TestLatestMatchingVersion(t *testing.T) { State: utils.Ptr(VersionStateDeprecated), }, }, + nil, utils.Ptr("1.19"), utils.Ptr("1.19.0"), true, @@ -472,6 +528,7 @@ func TestLatestMatchingVersion(t *testing.T) { State: utils.Ptr(VersionStateDeprecated), }, }, + nil, utils.Ptr("1.20"), utils.Ptr("1.20.0"), false, @@ -489,11 +546,48 @@ func TestLatestMatchingVersion(t *testing.T) { State: utils.Ptr(VersionStateSupported), }, }, + nil, utils.Ptr("1.20"), utils.Ptr("1.20.0"), false, true, }, + { + "deprecated_kubernetes_version_field", + []ske.KubernetesVersion{ + { + Version: utils.Ptr("1.20.0"), + State: utils.Ptr(VersionStateSupported), + }, + { + Version: utils.Ptr("1.19.0"), + State: utils.Ptr(VersionStateSupported), + }, + }, + utils.Ptr("1.20"), + nil, + utils.Ptr("1.20.0"), + false, + true, + }, + { + "nil_provided_version_get_latest", + []ske.KubernetesVersion{ + { + Version: utils.Ptr("1.20.0"), + State: utils.Ptr(VersionStateSupported), + }, + { + Version: utils.Ptr("1.19.0"), + State: utils.Ptr(VersionStateSupported), + }, + }, + nil, + nil, + utils.Ptr("1.20.0"), + false, + true, + }, { "no_matching_available_versions", []ske.KubernetesVersion{ @@ -506,14 +600,60 @@ func TestLatestMatchingVersion(t *testing.T) { State: utils.Ptr(VersionStateSupported), }, }, + nil, utils.Ptr("1.21"), nil, false, false, }, + { + "no_matching_available_versions_patch", + []ske.KubernetesVersion{ + { + Version: utils.Ptr("1.21.0"), + State: utils.Ptr(VersionStateSupported), + }, + { + Version: utils.Ptr("1.20.0"), + State: utils.Ptr(VersionStateSupported), + }, + { + Version: utils.Ptr("1.19.0"), + State: utils.Ptr(VersionStateSupported), + }, + }, + nil, + utils.Ptr("1.21.1"), + nil, + false, + false, + }, + { + "no_matching_available_versions_patch_2", + []ske.KubernetesVersion{ + { + Version: utils.Ptr("1.21.2"), + State: utils.Ptr(VersionStateSupported), + }, + { + Version: utils.Ptr("1.20.0"), + State: utils.Ptr(VersionStateSupported), + }, + { + Version: utils.Ptr("1.19.0"), + State: utils.Ptr(VersionStateSupported), + }, + }, + nil, + utils.Ptr("1.21.1"), + nil, + false, + false, + }, { "no_available_version", []ske.KubernetesVersion{}, + nil, utils.Ptr("1.20"), nil, false, @@ -522,6 +662,7 @@ func TestLatestMatchingVersion(t *testing.T) { { "nil_available_version", nil, + nil, utils.Ptr("1.20"), nil, false, @@ -539,32 +680,16 @@ func TestLatestMatchingVersion(t *testing.T) { State: utils.Ptr(VersionStateSupported), }, }, + nil, utils.Ptr(""), nil, false, false, }, - { - "nil_provided_version", - []ske.KubernetesVersion{ - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.19.0"), - State: utils.Ptr(VersionStateSupported), - }, - }, - nil, - nil, - false, - false, - }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - versionUsed, hasDeprecatedVersion, err := latestMatchingVersion(tt.availableVersions, tt.providedVersion) + versionUsed, hasDeprecatedVersion, err := latestMatchingVersion(tt.availableVersions, tt.providedVersion, tt.providedVersionMin) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } @@ -738,13 +863,13 @@ func TestCheckAllowPrivilegedContainers(t *testing.T) { isValid bool }{ { - description: "null_version_1", + description: "null_version_1_flag_deprecated", kubernetesVersion: nil, allowPrivilegeContainers: nil, - isValid: false, + isValid: true, }, { - description: "null_version_2", + description: "null_version_2_flag_deprecated", kubernetesVersion: nil, allowPrivilegeContainers: utils.Ptr(false), isValid: false, @@ -815,3 +940,72 @@ func TestCheckAllowPrivilegedContainers(t *testing.T) { }) } } + +func TestGetLatestSupportedVersion(t *testing.T) { + tests := []struct { + description string + listKubernetesVersion []ske.KubernetesVersion + isValid bool + expectedVersion *string + }{ + { + description: "base", + listKubernetesVersion: []ske.KubernetesVersion{ + { + State: utils.Ptr("supported"), + Version: utils.Ptr("1.2.3"), + }, + { + State: utils.Ptr("supported"), + Version: utils.Ptr("3.2.1"), + }, + { + State: utils.Ptr("not-supported"), + Version: utils.Ptr("4.4.4"), + }, + }, + isValid: true, + expectedVersion: utils.Ptr("3.2.1"), + }, + { + description: "no Kubernetes versions 1", + listKubernetesVersion: nil, + isValid: false, + }, + { + description: "no Kubernetes versions 2", + listKubernetesVersion: []ske.KubernetesVersion{}, + isValid: false, + }, + { + description: "no supported Kubernetes versions", + listKubernetesVersion: []ske.KubernetesVersion{ + { + State: utils.Ptr("not-supported"), + Version: utils.Ptr("1.2.3"), + }, + }, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + version, err := getLatestSupportedKubernetesVersion(tt.listKubernetesVersion) + + if tt.isValid && err != nil { + t.Errorf("failed on valid input") + } + if !tt.isValid && err == nil { + t.Errorf("did not fail on invalid input") + } + if !tt.isValid { + return + } + diff := cmp.Diff(version, tt.expectedVersion) + if diff != "" { + t.Fatalf("Output is not as expected: %s", diff) + } + }) + } +} diff --git a/stackit/internal/services/ske/ske_acc_test.go b/stackit/internal/services/ske/ske_acc_test.go index f96a659b..cfd3adba 100644 --- a/stackit/internal/services/ske/ske_acc_test.go +++ b/stackit/internal/services/ske/ske_acc_test.go @@ -3,37 +3,33 @@ package ske_test import ( "context" "fmt" - "net/http" + "strings" "testing" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" "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" ) -var projectResource = map[string]string{ - "project_id": testutil.ProjectId, -} - var clusterResource = map[string]string{ "project_id": testutil.ProjectId, "name": fmt.Sprintf("cl-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)), "name_min": fmt.Sprintf("cl-min-%s", acctest.RandStringFromCharSet(3, acctest.CharSetAlphaNum)), - "kubernetes_version": "1.25", - "kubernetes_version_used": "1.25.16", - "kubernetes_version_new": "1.26", - "kubernetes_version_used_new": "1.26.14", + "kubernetes_version_min": "1.26", + "kubernetes_version_used": "1.26.15", + "kubernetes_version_min_new": "1.27", + "kubernetes_version_used_new": "1.27.13", "nodepool_name": "np-acc-test", "nodepool_name_min": "np-acc-min-test", "nodepool_machine_type": "b1.2", - "nodepool_os_version": "3760.2.0", - "nodepool_os_version_min": "3760.2.0", + "nodepool_os_version": "3815.2.1", + "nodepool_os_version_min": "3815.2.1", "nodepool_os_name": "flatcar", "nodepool_minimum": "2", "nodepool_maximum": "3", @@ -71,14 +67,10 @@ func getConfig(version string, maintenanceEnd *string) string { return fmt.Sprintf(` %s - resource "stackit_ske_project" "project" { - project_id = "%s" - } - resource "stackit_ske_cluster" "cluster" { - project_id = stackit_ske_project.project.project_id + project_id = "%s" name = "%s" - kubernetes_version = "%s" + kubernetes_version_min = "%s" node_pools = [{ name = "%s" machine_type = "%s" @@ -125,15 +117,14 @@ func getConfig(version string, maintenanceEnd *string) string { } resource "stackit_ske_kubeconfig" "kubeconfig" { - project_id = stackit_ske_project.project.project_id + project_id = stackit_ske_cluster.cluster.project_id cluster_name = stackit_ske_cluster.cluster.name expiration = "%s" } resource "stackit_ske_cluster" "cluster_min" { - project_id = stackit_ske_project.project.project_id + project_id = "%s" name = "%s" - kubernetes_version = "%s" node_pools = [{ name = "%s" machine_type = "%s" @@ -151,7 +142,7 @@ func getConfig(version string, maintenanceEnd *string) string { } `, testutil.SKEProviderConfig(), - projectResource["project_id"], + clusterResource["project_id"], clusterResource["name"], version, clusterResource["nodepool_name"], @@ -187,8 +178,8 @@ func getConfig(version string, maintenanceEnd *string) string { clusterResource["kubeconfig_expiration"], // Minimal + clusterResource["project_id"], clusterResource["name_min"], - clusterResource["kubernetes_version_new"], clusterResource["nodepool_name_min"], clusterResource["nodepool_machine_type"], clusterResource["nodepool_os_version_min"], @@ -210,17 +201,11 @@ func TestAccSKE(t *testing.T) { // 1) Creation { - Config: getConfig(clusterResource["kubernetes_version"], nil), + Config: getConfig(clusterResource["kubernetes_version_min"], nil), Check: resource.ComposeAggregateTestCheckFunc( - // project data - resource.TestCheckResourceAttr("stackit_ske_project.project", "project_id", projectResource["project_id"]), // cluster data - resource.TestCheckResourceAttrPair( - "stackit_ske_project.project", "project_id", - "stackit_ske_cluster.cluster", "project_id", - ), resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "name", clusterResource["name"]), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "kubernetes_version", clusterResource["kubernetes_version"]), + resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "kubernetes_version_min", clusterResource["kubernetes_version_min"]), resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "kubernetes_version_used", clusterResource["kubernetes_version_used"]), resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.name", clusterResource["nodepool_name"]), resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.availability_zones.#", "1"), @@ -270,13 +255,8 @@ func TestAccSKE(t *testing.T) { resource.TestCheckResourceAttrSet("stackit_ske_kubeconfig.kubeconfig", "expires_at"), // Minimal cluster - resource.TestCheckResourceAttrPair( - "stackit_ske_project.project", "project_id", - "stackit_ske_cluster.cluster_min", "project_id", - ), resource.TestCheckResourceAttr("stackit_ske_cluster.cluster_min", "name", clusterResource["name_min"]), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster_min", "kubernetes_version", clusterResource["kubernetes_version_new"]), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster_min", "kubernetes_version_used", clusterResource["kubernetes_version_used_new"]), + resource.TestCheckResourceAttrSet("stackit_ske_cluster.cluster_min", "kubernetes_version_used"), resource.TestCheckNoResourceAttr("stackit_ske_cluster.cluster_min", "allow_privileged_containers"), resource.TestCheckResourceAttr("stackit_ske_cluster.cluster_min", "node_pools.0.name", clusterResource["nodepool_name_min"]), resource.TestCheckResourceAttr("stackit_ske_cluster.cluster_min", "node_pools.0.availability_zones.#", "1"), @@ -299,7 +279,7 @@ func TestAccSKE(t *testing.T) { resource.TestCheckResourceAttrSet("stackit_ske_cluster.cluster_min", "maintenance.enable_machine_image_version_updates"), resource.TestCheckResourceAttrSet("stackit_ske_cluster.cluster_min", "maintenance.start"), resource.TestCheckResourceAttrSet("stackit_ske_cluster.cluster_min", "maintenance.end"), - resource.TestCheckResourceAttrSet("stackit_ske_cluster.cluster_min", "kube_config"), + resource.TestCheckNoResourceAttr("stackit_ske_cluster.cluster_min", "kube_config"), ), }, // 2) Data source @@ -307,11 +287,6 @@ func TestAccSKE(t *testing.T) { Config: fmt.Sprintf(` %s - data "stackit_ske_project" "project" { - project_id = "%s" - depends_on = [stackit_ske_project.project] - } - data "stackit_ske_cluster" "cluster" { project_id = "%s" name = "%s" @@ -325,17 +300,13 @@ func TestAccSKE(t *testing.T) { } `, - getConfig(clusterResource["kubernetes_version"], nil), - projectResource["project_id"], + getConfig(clusterResource["kubernetes_version_min"], nil), clusterResource["project_id"], clusterResource["name"], clusterResource["project_id"], clusterResource["name_min"], ), Check: resource.ComposeAggregateTestCheckFunc( - // project data - resource.TestCheckResourceAttr("data.stackit_ske_project.project", "id", projectResource["project_id"]), - // cluster data resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "id", fmt.Sprintf("%s,%s", clusterResource["project_id"], @@ -343,7 +314,6 @@ func TestAccSKE(t *testing.T) { )), resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "project_id", clusterResource["project_id"]), resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "name", clusterResource["name"]), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "kubernetes_version", clusterResource["kubernetes_version"]), resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "kubernetes_version_used", clusterResource["kubernetes_version_used"]), resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.name", clusterResource["nodepool_name"]), resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.availability_zones.#", "1"), @@ -380,8 +350,7 @@ func TestAccSKE(t *testing.T) { // Minimal cluster resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster_min", "name", clusterResource["name_min"]), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster_min", "kubernetes_version", clusterResource["kubernetes_version_new"]), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster_min", "kubernetes_version_used", clusterResource["kubernetes_version_used_new"]), + resource.TestCheckResourceAttrSet("data.stackit_ske_cluster.cluster_min", "kubernetes_version_used"), resource.TestCheckNoResourceAttr("data.stackit_ske_cluster.cluster_min", "allow_privileged_containers"), resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster_min", "node_pools.0.name", clusterResource["nodepool_name_min"]), resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster_min", "node_pools.0.availability_zones.#", "1"), @@ -404,23 +373,10 @@ func TestAccSKE(t *testing.T) { resource.TestCheckResourceAttrSet("data.stackit_ske_cluster.cluster_min", "maintenance.enable_machine_image_version_updates"), resource.TestCheckResourceAttrSet("data.stackit_ske_cluster.cluster_min", "maintenance.start"), resource.TestCheckResourceAttrSet("data.stackit_ske_cluster.cluster_min", "maintenance.end"), - resource.TestCheckResourceAttrSet("data.stackit_ske_cluster.cluster_min", "kube_config"), + resource.TestCheckNoResourceAttr("data.stackit_ske_cluster.cluster_min", "kube_config"), ), }, - // 3) Import project - { - ResourceName: "stackit_ske_project.project", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - _, ok := s.RootModule().Resources["stackit_ske_project.project"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_ske_project.project") - } - return testutil.ProjectId, nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // 4) Import cluster + // 3) Import cluster { ResourceName: "stackit_ske_cluster.cluster", ImportStateIdFunc: func(s *terraform.State) (string, error) { @@ -441,9 +397,9 @@ func TestAccSKE(t *testing.T) { ImportState: true, ImportStateVerify: true, // The fields are not provided in the SKE API when disabled, although set actively. - ImportStateVerifyIgnore: []string{"kube_config", "extensions.argus.%", "extensions.argus.argus_instance_id", "extensions.argus.enabled", "extensions.acl.enabled", "extensions.acl.allowed_cidrs", "extensions.acl.allowed_cidrs.#", "extensions.acl.%"}, + ImportStateVerifyIgnore: []string{"kubernetes_version_min", "kube_config", "extensions.argus.%", "extensions.argus.argus_instance_id", "extensions.argus.enabled", "extensions.acl.enabled", "extensions.acl.allowed_cidrs", "extensions.acl.allowed_cidrs.#", "extensions.acl.%"}, }, - // 5) Import minimal cluster + // 4) Import minimal cluster { ResourceName: "stackit_ske_cluster.cluster_min", ImportStateIdFunc: func(s *terraform.State) (string, error) { @@ -463,16 +419,16 @@ func TestAccSKE(t *testing.T) { }, ImportState: true, ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"kube_config"}, + ImportStateVerifyIgnore: []string{"kubernetes_version_min", "kube_config"}, }, - // 6) Update kubernetes version and maximum + // 5) Update kubernetes version and maximum { - Config: getConfig(clusterResource["kubernetes_version_new"], utils.Ptr(clusterResource["maintenance_end_new"])), + Config: getConfig(clusterResource["kubernetes_version_min_new"], utils.Ptr(clusterResource["maintenance_end_new"])), Check: resource.ComposeAggregateTestCheckFunc( // cluster data resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "project_id", clusterResource["project_id"]), resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "name", clusterResource["name"]), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "kubernetes_version", clusterResource["kubernetes_version_new"]), + resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "kubernetes_version_min", clusterResource["kubernetes_version_min_new"]), resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "kubernetes_version_used", clusterResource["kubernetes_version_used_new"]), resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.name", clusterResource["nodepool_name"]), resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.availability_zones.#", "1"), @@ -509,14 +465,14 @@ func TestAccSKE(t *testing.T) { resource.TestCheckNoResourceAttr("stackit_ske_cluster.cluster", "kube_config"), // when using the kubeconfig resource, the kubeconfig field becomes null ), }, - // 7) Downgrade kubernetes version + // 6) Downgrade kubernetes version { - Config: getConfig(clusterResource["kubernetes_version"], utils.Ptr(clusterResource["maintenance_end_new"])), + Config: getConfig(clusterResource["kubernetes_version_min"], utils.Ptr(clusterResource["maintenance_end_new"])), Check: resource.ComposeAggregateTestCheckFunc( // cluster data resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "project_id", clusterResource["project_id"]), resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "name", clusterResource["name"]), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "kubernetes_version", clusterResource["kubernetes_version"]), + resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "kubernetes_version_min", clusterResource["kubernetes_version_min"]), resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "kubernetes_version_used", clusterResource["kubernetes_version_used"]), ), }, @@ -530,9 +486,7 @@ func testAccCheckSKEDestroy(s *terraform.State) error { var client *ske.APIClient var err error if testutil.SKECustomEndpoint == "" { - client, err = ske.NewAPIClient( - config.WithRegion("eu01"), - ) + client, err = ske.NewAPIClient() } else { client, err = ske.NewAPIClient( config.WithEndpoint(testutil.SKECustomEndpoint), @@ -542,34 +496,35 @@ func testAccCheckSKEDestroy(s *terraform.State) error { return fmt.Errorf("creating client: %w", err) } - projectsToDestroy := []string{} + clustersToDestroy := []string{} for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_ske_project" { + if rs.Type != "stackit_ske_cluster" { continue } - projectsToDestroy = append(projectsToDestroy, rs.Primary.ID) + // cluster terraform ID: = "[project_id],[cluster_name]" + clusterName := strings.Split(rs.Primary.ID, core.Separator)[1] + clustersToDestroy = append(clustersToDestroy, clusterName) } - for _, projectId := range projectsToDestroy { - _, err := client.GetServiceStatus(ctx, projectId).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if !ok { - return fmt.Errorf("could not convert error to GenericOpenApiError in acc test destruction, %w", err) - } - if oapiErr.StatusCode == http.StatusNotFound || oapiErr.StatusCode == http.StatusForbidden { - // Already gone - continue - } - return fmt.Errorf("getting project: %w", err) - } - _, err = client.DisableServiceExecute(ctx, projectId) - if err != nil { - return fmt.Errorf("destroying project %s during CheckDestroy: %w", projectId, err) + clustersResp, err := client.ListClusters(ctx, testutil.ProjectId).Execute() + if err != nil { + return fmt.Errorf("getting clustersResp: %w", err) + } + + items := *clustersResp.Items + for i := range items { + if items[i].Name == nil { + continue } - _, err = wait.DisableServiceWaitHandler(ctx, client, projectId).WaitWithContext(ctx) - if err != nil { - return fmt.Errorf("destroying project %s during CheckDestroy: waiting for deletion %w", projectId, err) + if utils.Contains(clustersToDestroy, *items[i].Name) { + _, err := client.DeleteClusterExecute(ctx, testutil.ProjectId, *items[i].Name) + if err != nil { + return fmt.Errorf("destroying cluster %s during CheckDestroy: %w", *items[i].Name, err) + } + _, err = wait.DeleteClusterWaitHandler(ctx, client, testutil.ProjectId, *items[i].Name).WaitWithContext(ctx) + if err != nil { + return fmt.Errorf("destroying cluster %s during CheckDestroy: waiting for deletion %w", *items[i].Name, err) + } } } return nil diff --git a/stackit/internal/validate/validate.go b/stackit/internal/validate/validate.go index 11925a94..d99f8df5 100644 --- a/stackit/internal/validate/validate.go +++ b/stackit/internal/validate/validate.go @@ -16,6 +16,11 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" ) +const ( + MajorMinorVersionRegex = `^\d+\.\d+?$` + FullVersionRegex = `^\d+\.\d+.\d+?$` +) + type Validator struct { description string markdownDescription string @@ -137,7 +142,7 @@ func MinorVersionNumber() *Validator { return &Validator{ description: description, validate: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { - exp := `^\d+\.\d+?$` + exp := MajorMinorVersionRegex r := regexp.MustCompile(exp) version := req.ConfigValue.ValueString() if !r.MatchString(version) { @@ -151,6 +156,30 @@ func MinorVersionNumber() *Validator { } } +func VersionNumber() *Validator { + description := "value must be a version number, without a leading 'v': '[MAJOR].[MINOR]' or '[MAJOR].[MINOR].[PATCH]'" + + return &Validator{ + description: description, + validate: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + minorVersionExp := MajorMinorVersionRegex + minorVersionRegex := regexp.MustCompile(minorVersionExp) + + versionExp := FullVersionRegex + versionRegex := regexp.MustCompile(versionExp) + + version := req.ConfigValue.ValueString() + if !minorVersionRegex.MatchString(version) && !versionRegex.MatchString(version) { + resp.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( + req.Path, + description, + req.ConfigValue.ValueString(), + )) + } + }, + } +} + func RFC3339SecondsOnly() *Validator { description := "value must be in RFC339 format (seconds only)" diff --git a/stackit/internal/validate/validate_test.go b/stackit/internal/validate/validate_test.go index c555b55c..661767d9 100644 --- a/stackit/internal/validate/validate_test.go +++ b/stackit/internal/validate/validate_test.go @@ -363,6 +363,80 @@ func TestMinorVersionNumber(t *testing.T) { } } +func TestVersionNumber(t *testing.T) { + tests := []struct { + description string + input string + isValid bool + }{ + { + "ok", + "1.20", + true, + }, + { + "ok-2", + "1.3", + true, + }, + { + "ok-3", + "10.1", + true, + }, + { + "ok-patch-version", + "1.20.1", + true, + }, + { + "ok-patch-version-2", + "1.20.10", + true, + }, + { + "ok-patch-version-3", + "10.20.10", + true, + }, + { + "Empty", + "", + false, + }, + { + "not ok", + "afssfdfs", + false, + }, + { + "not ok-major-version", + "1", + false, + }, + { + "not ok-version", + "v1.20.1", + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + r := validator.StringResponse{} + VersionNumber().ValidateString(context.Background(), validator.StringRequest{ + ConfigValue: types.StringValue(tt.input), + }, &r) + + if !tt.isValid && !r.Diagnostics.HasError() { + t.Fatalf("Should have failed") + } + if tt.isValid && r.Diagnostics.HasError() { + t.Fatalf("Should not have failed: %v", r.Diagnostics.Errors()) + } + }) + } +} + func TestRFC3339SecondsOnly(t *testing.T) { tests := []struct { description string