Support automatic kubernetes updates for stackit_ske_cluster (#360)

* new field kubernets_version_min and deprecate kubernetes_version

* Fix lint and tests

* Update acc test

* Deprecate datasource field, fix checkAllowPrivilegedContainers

* Update acc test, datasource and descriptions

* Update acc test

* Improve descriptions, fix bug

* Improve docs, fix acc test

* Update docs

* Update docs, fix acc test

* Update stackit/internal/services/ske/cluster/resource.go

Co-authored-by: Diogo Ferrão <diogo.ferrao@freiheit.com>

* Fix links

* Default ske auto-update to true

---------

Co-authored-by: Diogo Ferrão <diogo.ferrao@freiheit.com>
This commit is contained in:
Vicente Pinto 2024-05-14 14:55:33 +01:00 committed by GitHub
parent 94fbaf765c
commit 27b008a657
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 529 additions and 160 deletions

View file

@ -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,

View file

@ -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

View file

@ -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)
}
})
}
}

View file

@ -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

View file

@ -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)"

View file

@ -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