Fix kubernetes_version_min field logic (#363)

* 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

* Check current cluster version

* Add unit test

---------

Co-authored-by: Diogo Ferrão <diogo.ferrao@freiheit.com>
This commit is contained in:
Vicente Pinto 2024-05-17 12:12:35 +01:00 committed by GitHub
parent 56036e8704
commit 940b15e4b8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 308 additions and 43 deletions

View file

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

View file

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