diff --git a/docs/resources/ske_cluster.md b/docs/resources/ske_cluster.md index 66a5fd38..7c5e97f3 100644 --- a/docs/resources/ske_cluster.md +++ b/docs/resources/ske_cluster.md @@ -53,7 +53,7 @@ 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. +- `kubernetes_version_min` (String) The minimum Kubernetes version. This field will be used to set the minimum kubernetes version on creation/update of the cluster and can only by incremented. 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 diff --git a/stackit/internal/services/ske/cluster/resource.go b/stackit/internal/services/ske/cluster/resource.go index 8e84ba20..4451a7d4 100644 --- a/stackit/internal/services/ske/cluster/resource.go +++ b/stackit/internal/services/ske/cluster/resource.go @@ -59,6 +59,10 @@ var ( _ resource.ResourceWithImportState = &clusterResource{} ) +type skeClient interface { + GetClusterExecute(ctx context.Context, projectId, clusterName string) (*ske.Cluster, error) +} + type Model struct { Id types.String `tfsdk:"id"` // needed by TF ProjectId types.String `tfsdk:"project_id"` @@ -275,19 +279,8 @@ func (r *clusterResource) Schema(_ context.Context, _ resource.SchemaRequest, re }, }, "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.", + Description: "The minimum Kubernetes version. This field will be used to set the minimum kubernetes version on creation/update of the cluster and can only by incremented. 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(), }, @@ -631,7 +624,7 @@ func (r *clusterResource) Create(ctx context.Context, req resource.CreateRequest return } - r.createOrUpdateCluster(ctx, &resp.Diagnostics, &model, availableVersions) + r.createOrUpdateCluster(ctx, &resp.Diagnostics, &model, availableVersions, nil) if resp.Diagnostics.HasError() { return } @@ -659,11 +652,26 @@ func (r *clusterResource) loadAvailableVersions(ctx context.Context) ([]ske.Kube return *res.KubernetesVersions, nil } -func (r *clusterResource) createOrUpdateCluster(ctx context.Context, diags *diag.Diagnostics, model *Model, availableVersions []ske.KubernetesVersion) { +// getCurrentKubernetesVersion makes a call to get the details of a cluster and returns the current kubernetes version +// if the cluster doesn't exist or some error occurs, returns nil +func getCurrentKubernetesVersion(ctx context.Context, c skeClient, m *Model) *string { + res, err := c.GetClusterExecute(ctx, m.ProjectId.ValueString(), m.Name.ValueString()) + if err != nil { + return nil + } + + if res != nil && res.Kubernetes != nil { + return res.Kubernetes.Version + } + + return nil +} + +func (r *clusterResource) createOrUpdateCluster(ctx context.Context, diags *diag.Diagnostics, model *Model, availableVersions []ske.KubernetesVersion, currentKubernetesVersion *string) { // cluster vars projectId := model.ProjectId.ValueString() name := model.Name.ValueString() - kubernetes, hasDeprecatedVersion, err := toKubernetesPayload(model, availableVersions) + kubernetes, hasDeprecatedVersion, err := toKubernetesPayload(model, availableVersions, currentKubernetesVersion) if err != nil { core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Creating cluster config API payload: %v", err)) return @@ -1373,8 +1381,18 @@ func mapExtensions(ctx context.Context, cl *ske.Cluster, m *Model) error { return nil } -func toKubernetesPayload(m *Model, availableVersions []ske.KubernetesVersion) (kubernetesPayload *ske.Kubernetes, hasDeprecatedVersion bool, err error) { - versionUsed, hasDeprecatedVersion, err := latestMatchingVersion(availableVersions, conversion.StringValueToPointer(m.KubernetesVersion), conversion.StringValueToPointer(m.KubernetesVersionMin)) +func toKubernetesPayload(m *Model, availableVersions []ske.KubernetesVersion, currentKubernetesVersion *string) (kubernetesPayload *ske.Kubernetes, hasDeprecatedVersion bool, err error) { + providedVersionMin := m.KubernetesVersionMin.ValueStringPointer() + + if !m.KubernetesVersion.IsNull() { + // kubernetes_version field deprecation + // this if clause should be removed once kubernetes_version field is completely removed + // kubernetes_version field value is used as minimum kubernetes version + // and currenteKubernetesVersion is ignored + providedVersionMin = conversion.StringValueToPointer(m.KubernetesVersion) + } + + versionUsed, hasDeprecatedVersion, err := latestMatchingVersion(availableVersions, providedVersionMin, currentKubernetesVersion) if err != nil { return nil, false, fmt.Errorf("getting latest matching kubernetes version: %w", err) } @@ -1386,36 +1404,44 @@ func toKubernetesPayload(m *Model, availableVersions []ske.KubernetesVersion) (k return k, hasDeprecatedVersion, nil } -func latestMatchingVersion(availableVersions []ske.KubernetesVersion, providedVersion, providedVersionMin *string) (version *string, deprecated bool, err error) { +func latestMatchingVersion(availableVersions []ske.KubernetesVersion, kubernetesVersionMin, currentKubernetesVersion *string) (version *string, deprecated bool, err error) { deprecated = false if availableVersions == nil { return nil, false, fmt.Errorf("nil available kubernetes versions") } - if providedVersionMin == nil { - if providedVersion == nil { - // kubernetes_version field deprecation - // this if clause should be removed once kubernetes_version field is completely removed + if kubernetesVersionMin == nil { + if currentKubernetesVersion == nil { 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 + kubernetesVersionMin = currentKubernetesVersion + } else if currentKubernetesVersion != nil { + // For an already existing cluster, if kubernetes_version_min is set to a lower version than what is being used in the cluster + // return the currently used version + kubernetesVersionUsed := *currentKubernetesVersion + kubernetesVersionMinString := *kubernetesVersionMin + + minVersionPrefixed := "v" + kubernetesVersionMinString + usedVersionPrefixed := "v" + kubernetesVersionUsed + + if semver.Compare(minVersionPrefixed, usedVersionPrefixed) == -1 { + kubernetesVersionMin = currentKubernetesVersion + } } var fullVersion bool versionExp := validate.FullVersionRegex versionRegex := regexp.MustCompile(versionExp) - if versionRegex.MatchString(*providedVersionMin) { + if versionRegex.MatchString(*kubernetesVersionMin) { fullVersion = true } - providedVersionPrefixed := "v" + *providedVersionMin + providedVersionPrefixed := "v" + *kubernetesVersionMin if !semver.IsValid(providedVersionPrefixed) { return nil, false, fmt.Errorf("provided version is invalid") @@ -1565,7 +1591,9 @@ func (r *clusterResource) Update(ctx context.Context, req resource.UpdateRequest return } - r.createOrUpdateCluster(ctx, &resp.Diagnostics, &model, availableVersions) + currentKubernetesVersion := getCurrentKubernetesVersion(ctx, r.client, &model) + + r.createOrUpdateCluster(ctx, &resp.Diagnostics, &model, availableVersions, currentKubernetesVersion) 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 07f31c94..93eb6cba 100644 --- a/stackit/internal/services/ske/cluster/resource_test.go +++ b/stackit/internal/services/ske/cluster/resource_test.go @@ -2,6 +2,7 @@ package ske import ( "context" + "fmt" "testing" "github.com/google/go-cmp/cmp" @@ -12,6 +13,19 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" ) +type skeClientMocked struct { + returnError bool + getClusterResp *ske.Cluster +} + +func (c *skeClientMocked) GetClusterExecute(_ context.Context, _, _ string) (*ske.Cluster, error) { + if c.returnError { + return nil, fmt.Errorf("get cluster failed") + } + + return c.getClusterResp, nil +} + func TestMapFields(t *testing.T) { cs := ske.ClusterStatusState("OK") tests := []struct { @@ -396,8 +410,8 @@ func TestLatestMatchingVersion(t *testing.T) { tests := []struct { description string availableVersions []ske.KubernetesVersion - providedVersion *string - providedVersionMin *string + kubernetesVersionMin *string + currentKubernetesVersion *string expectedVersionUsed *string expectedHasDeprecatedVersion bool isValid bool @@ -422,8 +436,8 @@ func TestLatestMatchingVersion(t *testing.T) { State: utils.Ptr(VersionStateSupported), }, }, - nil, utils.Ptr("1.20.1"), + nil, utils.Ptr("1.20.1"), false, true, @@ -448,8 +462,8 @@ func TestLatestMatchingVersion(t *testing.T) { State: utils.Ptr(VersionStateSupported), }, }, - nil, utils.Ptr("1.20.0"), + nil, utils.Ptr("1.20.0"), false, true, @@ -474,8 +488,8 @@ func TestLatestMatchingVersion(t *testing.T) { State: utils.Ptr(VersionStateSupported), }, }, - nil, utils.Ptr("1.20"), + nil, utils.Ptr("1.20.2"), false, true, @@ -492,8 +506,8 @@ func TestLatestMatchingVersion(t *testing.T) { State: utils.Ptr(VersionStateSupported), }, }, - nil, utils.Ptr("1.20"), + nil, utils.Ptr("1.20.0"), false, true, @@ -510,8 +524,8 @@ func TestLatestMatchingVersion(t *testing.T) { State: utils.Ptr(VersionStateDeprecated), }, }, - nil, utils.Ptr("1.19"), + nil, utils.Ptr("1.19.0"), true, true, @@ -528,8 +542,8 @@ func TestLatestMatchingVersion(t *testing.T) { State: utils.Ptr(VersionStateDeprecated), }, }, - nil, utils.Ptr("1.20"), + nil, utils.Ptr("1.20.0"), false, true, @@ -546,8 +560,120 @@ func TestLatestMatchingVersion(t *testing.T) { State: utils.Ptr(VersionStateSupported), }, }, - nil, 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, + }, + { + "nil_provided_version_use_current", + []ske.KubernetesVersion{ + { + Version: utils.Ptr("1.20.0"), + State: utils.Ptr(VersionStateSupported), + }, + { + Version: utils.Ptr("1.19.0"), + State: utils.Ptr(VersionStateSupported), + }, + }, + nil, + utils.Ptr("1.19.0"), + utils.Ptr("1.19.0"), + false, + true, + }, + { + "update_lower_min_provided", + []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.19"), + utils.Ptr("1.20.0"), + utils.Ptr("1.20.0"), + false, + true, + }, + { + "update_lower_min_provided_deprecated_version", + []ske.KubernetesVersion{ + { + Version: utils.Ptr("1.21.0"), + State: utils.Ptr(VersionStateSupported), + }, + { + Version: utils.Ptr("1.20.0"), + State: utils.Ptr(VersionStateDeprecated), + }, + { + Version: utils.Ptr("1.19.0"), + State: utils.Ptr(VersionStateDeprecated), + }, + }, + utils.Ptr("1.19"), + utils.Ptr("1.20.0"), + utils.Ptr("1.20.0"), + true, + true, + }, + { + "update_matching_min_provided", + []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"), + utils.Ptr("1.20.0"), + utils.Ptr("1.20.0"), + false, + true, + }, + { + "update_higher_min_provided", + []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"), + utils.Ptr("1.19.0"), utils.Ptr("1.20.0"), false, true, @@ -600,9 +726,53 @@ func TestLatestMatchingVersion(t *testing.T) { State: utils.Ptr(VersionStateSupported), }, }, - nil, utils.Ptr("1.21"), nil, + 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), + }, + }, + utils.Ptr("1.21.1"), + nil, + 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), + }, + }, + utils.Ptr("1.21.1"), + nil, + nil, false, false, }, @@ -653,18 +823,18 @@ func TestLatestMatchingVersion(t *testing.T) { { "no_available_version", []ske.KubernetesVersion{}, - nil, utils.Ptr("1.20"), nil, + nil, false, false, }, { "nil_available_version", nil, - nil, utils.Ptr("1.20"), nil, + nil, false, false, }, @@ -680,16 +850,16 @@ func TestLatestMatchingVersion(t *testing.T) { State: utils.Ptr(VersionStateSupported), }, }, - nil, utils.Ptr(""), nil, + nil, false, false, }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - versionUsed, hasDeprecatedVersion, err := latestMatchingVersion(tt.availableVersions, tt.providedVersion, tt.providedVersionMin) + versionUsed, hasDeprecatedVersion, err := latestMatchingVersion(tt.availableVersions, tt.kubernetesVersionMin, tt.currentKubernetesVersion) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } @@ -941,6 +1111,73 @@ func TestCheckAllowPrivilegedContainers(t *testing.T) { } } +func TestGetCurrentKubernetesVersion(t *testing.T) { + tests := []struct { + description string + mockedResp *ske.Cluster + expected *string + getClusterFails bool + }{ + { + "ok", + &ske.Cluster{ + Kubernetes: &ske.Kubernetes{ + Version: utils.Ptr("v1.0.0"), + }, + }, + utils.Ptr("v1.0.0"), + false, + }, + { + "get fails", + nil, + nil, + true, + }, + { + "nil version", + &ske.Cluster{ + Kubernetes: &ske.Kubernetes{ + Version: nil, + }, + }, + nil, + false, + }, + { + "nil kubernetes", + &ske.Cluster{ + Kubernetes: nil, + }, + nil, + false, + }, + { + "nil response", + nil, + nil, + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + client := &skeClientMocked{ + returnError: tt.getClusterFails, + getClusterResp: tt.mockedResp, + } + model := &Model{ + ProjectId: types.StringValue("pid"), + Name: types.StringValue("name"), + } + version := getCurrentKubernetesVersion(context.Background(), client, model) + diff := cmp.Diff(version, tt.expected) + if diff != "" { + t.Fatalf("Version does not match: %s", diff) + } + }) + } +} + func TestGetLatestSupportedVersion(t *testing.T) { tests := []struct { description string