From e850a952bd1af8ef96dc5b6f7f3a02e4713ccf9d Mon Sep 17 00:00:00 2001 From: Vicente Pinto Date: Mon, 20 May 2024 18:03:50 +0100 Subject: [PATCH] Support SKE OS versions auto and forceful updates (#364) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Deprecate os_version and add os_version_used * Add os_version_min field * Draft implementation for os_version_min * Finish implementation * Fix mapFields * Update stackit/internal/services/ske/cluster/resource.go Co-authored-by: Diogo Ferrão * Update stackit/internal/services/ske/cluster/resource.go Co-authored-by: Diogo Ferrão * Update stackit/internal/services/ske/cluster/resource.go Co-authored-by: Diogo Ferrão * Update stackit/internal/services/ske/cluster/resource.go Co-authored-by: Diogo Ferrão * Improve descriptions * Improve variable names * Fix lint * Extend acc test, generate docs * Don't select automatically preview versions * Update acc test * Simplify code --------- Co-authored-by: Diogo Ferrão --- docs/data-sources/ske_cluster.md | 2 + docs/resources/ske_cluster.md | 13 +- .../services/ske/cluster/datasource.go | 8 + .../internal/services/ske/cluster/resource.go | 316 ++++++- .../services/ske/cluster/resource_test.go | 848 ++++++++++++++++-- stackit/internal/services/ske/ske_acc_test.go | 46 +- 6 files changed, 1080 insertions(+), 153 deletions(-) diff --git a/docs/data-sources/ske_cluster.md b/docs/data-sources/ske_cluster.md index 64bde65e..ae41da4e 100644 --- a/docs/data-sources/ske_cluster.md +++ b/docs/data-sources/ske_cluster.md @@ -106,6 +106,8 @@ Read-Only: - `name` (String) Specifies the name of the node pool. - `os_name` (String) The name of the OS image. - `os_version` (String) The OS image version. +- `os_version_min` (String) The minimum OS image 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 OS image version being used for the node pool, use the read-only `os_version_used` field. +- `os_version_used` (String) Full OS image version used. For example, if 3815.2 was set in `os_version_min`, this value may result to 3815.2.2. 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). - `taints` (Attributes List) Specifies a taint list as defined below. (see [below for nested schema](#nestedatt--node_pools--taints)) - `volume_size` (Number) The volume size in GB. - `volume_type` (String) Specifies the volume type. diff --git a/docs/resources/ske_cluster.md b/docs/resources/ske_cluster.md index 7c5e97f3..1db3c853 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 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. +- `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. 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 @@ -72,7 +72,6 @@ Required: - `maximum` (Number) Maximum number of nodes in the pool. - `minimum` (Number) Minimum number of nodes in the pool. - `name` (String) Specifies the name of the node pool. -- `os_version` (String) The OS image version. Optional: @@ -81,10 +80,16 @@ Optional: - `max_surge` (Number) Maximum number of additional VMs that are created during an update. - `max_unavailable` (Number) Maximum number of VMs that that can be unavailable during an update. - `os_name` (String) The name of the OS image. E.g. `flatcar`. +- `os_version` (String, Deprecated) This field is deprecated, use `os_version_min` to configure the version and `os_version_used` to get the currently used version instead +- `os_version_min` (String) The minimum OS image version. This field will be used to set the minimum OS image version on creation/update of the cluster. If unset, the latest supported OS image 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 OS image version being used for the node pool, use the read-only `os_version_used` field. - `taints` (Attributes List) Specifies a taint list as defined below. (see [below for nested schema](#nestedatt--node_pools--taints)) - `volume_size` (Number) The volume size in GB. E.g. `20` - `volume_type` (String) Specifies the volume type. E.g. `storage_premium_perf1`. +Read-Only: + +- `os_version_used` (String) Full OS image version used. For example, if 3815.2 was set in `os_version_min`, this value may result to 3815.2.2. 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.taints` @@ -150,10 +155,10 @@ Optional: Required: -- `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). +- `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). +- `enable_machine_image_version_updates` (Boolean) Flag to enable/disable auto-updates of the OS image version. Defaults to `true`. SKE automatically updates the cluster Kubernetes version if you have set `maintenance.enable_kubernetes_version_updates` to true or if there is a mandatory update, as described in [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 8d20c397..5fa217e7 100644 --- a/stackit/internal/services/ske/cluster/datasource.go +++ b/stackit/internal/services/ske/cluster/datasource.go @@ -130,10 +130,18 @@ func (r *clusterDataSource) Schema(_ context.Context, _ datasource.SchemaRequest Description: "The name of the OS image.", Computed: true, }, + "os_version_min": schema.StringAttribute{ + Description: "The minimum OS image version, this field is always nil. " + SKEUpdateDoc + " To get the current OS image version being used for the node pool, use the read-only `os_version_used` field.", + Computed: true, + }, "os_version": schema.StringAttribute{ Description: "The OS image version.", Computed: true, }, + "os_version_used": schema.StringAttribute{ + Description: "Full OS image version used. For example, if 3815.2 was set in `os_version_min`, this value may result to 3815.2.2. " + SKEUpdateDoc, + Computed: true, + }, "minimum": schema.Int64Attribute{ Description: "Minimum number of nodes in the pool.", Computed: true, diff --git a/stackit/internal/services/ske/cluster/resource.go b/stackit/internal/services/ske/cluster/resource.go index 4451a7d4..a5ff305f 100644 --- a/stackit/internal/services/ske/cluster/resource.go +++ b/stackit/internal/services/ske/cluster/resource.go @@ -83,7 +83,9 @@ type nodePool struct { Name types.String `tfsdk:"name"` MachineType types.String `tfsdk:"machine_type"` OSName types.String `tfsdk:"os_name"` + OSVersionMin types.String `tfsdk:"os_version_min"` OSVersion types.String `tfsdk:"os_version"` + OSVersionUsed types.String `tfsdk:"os_version_used"` Minimum types.Int64 `tfsdk:"minimum"` Maximum types.Int64 `tfsdk:"maximum"` MaxSurge types.Int64 `tfsdk:"max_surge"` @@ -101,7 +103,9 @@ var nodePoolTypes = map[string]attr.Type{ "name": basetypes.StringType{}, "machine_type": basetypes.StringType{}, "os_name": basetypes.StringType{}, + "os_version_min": basetypes.StringType{}, "os_version": basetypes.StringType{}, + "os_version_used": basetypes.StringType{}, "minimum": basetypes.Int64Type{}, "maximum": basetypes.Int64Type{}, "max_surge": basetypes.Int64Type{}, @@ -279,7 +283,7 @@ 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 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.", + Description: "The minimum Kubernetes version. This field will be used to set the minimum kubernetes version on creation/update 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, Validators: []validator.String{ validate.VersionNumber(), @@ -288,7 +292,7 @@ func (r *clusterResource) Schema(_ context.Context, _ resource.SchemaRequest, re "kubernetes_version": schema.StringAttribute{ 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.", + DeprecationMessage: "Use `kubernetes_version_min instead`. Setting a specific kubernetes version would cause errors during minor version upgrades due to forced 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() { @@ -376,9 +380,21 @@ func (r *clusterResource) Schema(_ context.Context, _ resource.SchemaRequest, re Computed: true, Default: stringdefault.StaticString(DefaultOSName), }, + "os_version_min": schema.StringAttribute{ + Description: "The minimum OS image version. This field will be used to set the minimum OS image version on creation/update of the cluster. If unset, the latest supported OS image version will be used. " + SKEUpdateDoc + " To get the current OS image version being used for the node pool, use the read-only `os_version_used` field.", + Optional: true, + Validators: []validator.String{ + validate.VersionNumber(), + }, + }, "os_version": schema.StringAttribute{ - Description: "The OS image version.", - Required: true, + Description: "This field is deprecated, use `os_version_min` to configure the version and `os_version_used` to get the currently used version instead", + DeprecationMessage: "Use `os_version_min` to configure the version and `os_version_used` to get the currently used version instead. Setting a specific OS image version will cause errors during minor OS upgrades due to forced updates.", + Optional: true, + }, + "os_version_used": schema.StringAttribute{ + Description: "Full OS image version used. For example, if 3815.2 was set in `os_version_min`, this value may result to 3815.2.2. " + SKEUpdateDoc, + Computed: true, }, "volume_type": schema.StringAttribute{ Description: "Specifies the volume type. E.g. `storage_premium_perf1`.", @@ -442,14 +458,16 @@ 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. Defaults to `true. " + SKEUpdateDoc, + 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.", - Required: true, + Description: "Flag to enable/disable auto-updates of the OS image version. Defaults to `true`. " + SKEUpdateDoc, + Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), }, "start": schema.StringAttribute{ Description: "Time for maintenance window start. E.g. `01:23:45Z`, `05:00:00+02:00`.", @@ -549,6 +567,10 @@ func (r *clusterResource) ConfigValidators(_ context.Context) []resource.ConfigV path.MatchRoot("kubernetes_version"), path.MatchRoot("kubernetes_version_min"), ), + resourcevalidator.Conflicting( + path.MatchRoot("node_pools").AtAnyListIndex().AtName("os_version_min"), + path.MatchRoot("node_pools").AtAnyListIndex().AtName("os_version"), + ), } } @@ -618,13 +640,13 @@ func (r *clusterResource) Create(ctx context.Context, req resource.CreateRequest return } - availableVersions, err := r.loadAvailableVersions(ctx) + availableKubernetesVersions, availableMachines, err := r.loadAvailableVersions(ctx) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating cluster", fmt.Sprintf("Loading available Kubernetes versions: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating cluster", fmt.Sprintf("Loading available Kubernetes and machine image versions: %v", err)) return } - r.createOrUpdateCluster(ctx, &resp.Diagnostics, &model, availableVersions, nil) + r.createOrUpdateCluster(ctx, &resp.Diagnostics, &model, availableKubernetesVersions, availableMachines, nil, nil) if resp.Diagnostics.HasError() { return } @@ -638,40 +660,57 @@ func (r *clusterResource) Create(ctx context.Context, req resource.CreateRequest tflog.Info(ctx, "SKE cluster created") } -func (r *clusterResource) loadAvailableVersions(ctx context.Context) ([]ske.KubernetesVersion, error) { +func (r *clusterResource) loadAvailableVersions(ctx context.Context) ([]ske.KubernetesVersion, []ske.MachineImage, error) { c := r.client res, err := c.ListProviderOptions(ctx).Execute() if err != nil { - return nil, fmt.Errorf("calling API: %w", err) + return nil, nil, fmt.Errorf("calling API: %w", err) } if res.KubernetesVersions == nil { - return nil, fmt.Errorf("API response has nil kubernetesVersions") + return nil, nil, fmt.Errorf("API response has nil kubernetesVersions") } - return *res.KubernetesVersions, nil + if res.MachineImages == nil { + return nil, nil, fmt.Errorf("API response has nil machine images") + } + + return *res.KubernetesVersions, *res.MachineImages, nil } -// 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 { +// getCurrentVersions makes a call to get the details of a cluster and returns the current kubernetes version and a +// a map with the machine image for each nodepool, which can be used to check the current machine image versions. +// if the cluster doesn't exist or some error occurs, returns nil for both +func getCurrentVersions(ctx context.Context, c skeClient, m *Model) (kubernetesVersion *string, nodePoolMachineImages map[string]*ske.Image) { res, err := c.GetClusterExecute(ctx, m.ProjectId.ValueString(), m.Name.ValueString()) - if err != nil { - return nil + if err != nil || res == nil { + return nil, nil } - if res != nil && res.Kubernetes != nil { - return res.Kubernetes.Version + if res.Kubernetes != nil { + kubernetesVersion = res.Kubernetes.Version } - return nil + if res.Nodepools == nil { + return kubernetesVersion, nil + } + + nodePoolMachineImages = map[string]*ske.Image{} + for _, nodePool := range *res.Nodepools { + if nodePool.Name == nil || nodePool.Machine == nil || nodePool.Machine.Image == nil || nodePool.Machine.Image.Name == nil { + continue + } + nodePoolMachineImages[*nodePool.Name] = nodePool.Machine.Image + } + + return kubernetesVersion, nodePoolMachineImages } -func (r *clusterResource) createOrUpdateCluster(ctx context.Context, diags *diag.Diagnostics, model *Model, availableVersions []ske.KubernetesVersion, currentKubernetesVersion *string) { +func (r *clusterResource) createOrUpdateCluster(ctx context.Context, diags *diag.Diagnostics, model *Model, availableKubernetesVersions []ske.KubernetesVersion, availableMachineVersions []ske.MachineImage, currentKubernetesVersion *string, currentMachineImages map[string]*ske.Image) { // cluster vars projectId := model.ProjectId.ValueString() name := model.Name.ValueString() - kubernetes, hasDeprecatedVersion, err := toKubernetesPayload(model, availableVersions, currentKubernetesVersion) + kubernetes, hasDeprecatedVersion, err := toKubernetesPayload(model, availableKubernetesVersions, currentKubernetesVersion) if err != nil { core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Creating cluster config API payload: %v", err)) return @@ -679,11 +718,14 @@ func (r *clusterResource) createOrUpdateCluster(ctx context.Context, diags *diag if hasDeprecatedVersion { diags.AddWarning("Deprecated Kubernetes version", fmt.Sprintf("Version %s of Kubernetes is deprecated, please update it", *kubernetes.Version)) } - nodePools, err := toNodepoolsPayload(ctx, model) + nodePools, deprecatedVersionsUsed, err := toNodepoolsPayload(ctx, model, availableMachineVersions, currentMachineImages) if err != nil { core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Creating node pools API payload: %v", err)) return } + if len(deprecatedVersionsUsed) != 0 { + diags.AddWarning("Deprecated node pools OS versions used", fmt.Sprintf("The following versions of machines are deprecated, please update them: [%s]", strings.Join(deprecatedVersionsUsed, ","))) + } maintenance, err := toMaintenancePayload(ctx, model) if err != nil { core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Creating maintenance API payload: %v", err)) @@ -763,14 +805,15 @@ func (r *clusterResource) getCredential(ctx context.Context, diags *diag.Diagnos return nil } -func toNodepoolsPayload(ctx context.Context, m *Model) ([]ske.Nodepool, error) { +func toNodepoolsPayload(ctx context.Context, m *Model, availableMachineVersions []ske.MachineImage, currentMachineImages map[string]*ske.Image) ([]ske.Nodepool, []string, error) { nodePools := []nodePool{} diags := m.NodePools.ElementsAs(ctx, &nodePools, false) if diags.HasError() { - return nil, core.DiagsToError(diags) + return nil, nil, core.DiagsToError(diags) } cnps := []ske.Nodepool{} + deprecatedVersionsUsed := []string{} for i := range nodePools { nodePool := nodePools[i] @@ -778,7 +821,7 @@ func toNodepoolsPayload(ctx context.Context, m *Model) ([]ske.Nodepool, error) { taintsModel := []taint{} diags := nodePool.Taints.ElementsAs(ctx, &taintsModel, false) if diags.HasError() { - return nil, core.DiagsToError(diags) + return nil, nil, core.DiagsToError(diags) } ts := []ske.Taint{} @@ -824,8 +867,36 @@ func toNodepoolsPayload(ctx context.Context, m *Model) ([]ske.Nodepool, error) { cn := ske.CRI{ Name: conversion.StringValueToPointer(nodePool.CRI), } + + providedVersionMin := conversion.StringValueToPointer(nodePool.OSVersionMin) + if !nodePool.OSVersion.IsNull() { + // os_version field deprecation + // this if clause should be removed once os_version field is completely removed + // os_version field value is used as minimum os version + providedVersionMin = conversion.StringValueToPointer(nodePool.OSVersion) + } + + name := conversion.StringValueToPointer(nodePool.Name) + machineOSName := conversion.StringValueToPointer(nodePool.OSName) + if name == nil { + return nil, nil, fmt.Errorf("found nil node pool name for node_pool[%d]", i) + } + if machineOSName == nil { + return nil, nil, fmt.Errorf("found nil machine name for node_pool %q", *name) + } + + currentMachineImage := currentMachineImages[*name] + + machineVersion, hasDeprecatedVersion, err := latestMatchingMachineVersion(availableMachineVersions, providedVersionMin, *machineOSName, currentMachineImage) + if err != nil { + return nil, nil, fmt.Errorf("getting latest matching machine image version: %w", err) + } + if hasDeprecatedVersion && machineVersion != nil { + deprecatedVersionsUsed = append(deprecatedVersionsUsed, *machineVersion) + } + cnp := ske.Nodepool{ - Name: conversion.StringValueToPointer(nodePool.Name), + Name: name, Minimum: conversion.Int64ValueToPointer(nodePool.Minimum), Maximum: conversion.Int64ValueToPointer(nodePool.Maximum), MaxSurge: conversion.Int64ValueToPointer(nodePool.MaxSurge), @@ -833,8 +904,8 @@ func toNodepoolsPayload(ctx context.Context, m *Model) ([]ske.Nodepool, error) { Machine: &ske.Machine{ Type: conversion.StringValueToPointer(nodePool.MachineType), Image: &ske.Image{ - Name: conversion.StringValueToPointer(nodePool.OSName), - Version: conversion.StringValueToPointer(nodePool.OSVersion), + Name: machineOSName, + Version: machineVersion, }, }, Volume: &ske.Volume{ @@ -848,7 +919,140 @@ func toNodepoolsPayload(ctx context.Context, m *Model) ([]ske.Nodepool, error) { } cnps = append(cnps, cnp) } - return cnps, nil + return cnps, deprecatedVersionsUsed, nil +} + +// latestMatchingMachineVersion determines the latest machine image version for the create/update payload. +// It considers the available versions for the specified OS (OSName), the minimum version configured by the user, +// and the current version in the cluster. The function's behavior is as follows: +// +// 1. If the minimum version is not set: +// - Return the current version if it exists. +// - Otherwise, return the latest available version for the specified OS. +// +// 2. If the minimum version is set: +// - If the minimum version is a downgrade, use the current version instead. +// - If a patch is not specified for the minimum version, return the latest patch for that minor version. +// +// 3. For the selected version, check its state and return it, indicating if it is deprecated or not. +func latestMatchingMachineVersion(availableImages []ske.MachineImage, versionMin *string, osName string, currentImage *ske.Image) (version *string, deprecated bool, err error) { + deprecated = false + + if availableImages == nil { + return nil, false, fmt.Errorf("nil available machine versions") + } + + var availableMachineVersions []ske.MachineImageVersion + for _, machine := range availableImages { + if machine.Name != nil && *machine.Name == osName && machine.Versions != nil { + availableMachineVersions = *machine.Versions + } + } + + if len(availableImages) == 0 { + return nil, false, fmt.Errorf("there are no available machine versions for the provided machine image name %s", osName) + } + + if versionMin == nil { + // Different machine OSes have different versions. + // If the current machine image is nil or the machine image name has been updated, + // retrieve the latest supported version. Otherwise, use the current machine version. + if currentImage == nil || currentImage.Name == nil || *currentImage.Name != osName { + latestVersion, err := getLatestSupportedMachineVersion(availableMachineVersions) + if err != nil { + return nil, false, fmt.Errorf("get latest supported machine image version: %w", err) + } + return latestVersion, false, nil + } + versionMin = currentImage.Version + } else if currentImage != nil && currentImage.Name != nil && *currentImage.Name == osName { + // If the os_version_min is set but is lower than the current version used in the cluster, + // retain the current version to avoid downgrading. + minimumVersion := "v" + *versionMin + currentVersion := "v" + *currentImage.Version + + if semver.Compare(minimumVersion, currentVersion) == -1 { + versionMin = currentImage.Version + } + } + + var fullVersion bool + versionExp := validate.FullVersionRegex + versionRegex := regexp.MustCompile(versionExp) + if versionRegex.MatchString(*versionMin) { + fullVersion = true + } + + providedVersionPrefixed := "v" + *versionMin + + 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, minor and patch version provided by the user + for _, v := range availableMachineVersions { + if v.State == nil || v.Version == nil { + continue + } + availableVersionsArray = append(availableVersionsArray, *v.Version) + vPreffixed := "v" + *v.Version + + 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) && + (v.State != nil && *v.State != VersionStatePreview) { + versionUsed = v.Version + state = v.State + } + } + } + + if versionUsed != nil { + deprecated = strings.EqualFold(*state, VersionStateDeprecated) + } + + // Throwing error if we could not match the version with the available versions + if versionUsed == nil { + return nil, false, fmt.Errorf("provided version is not one of the available machine image versions, available versions are: %s", strings.Join(availableVersionsArray, ",")) + } + + return versionUsed, deprecated, nil +} + +func getLatestSupportedMachineVersion(versions []ske.MachineImageVersion) (*string, error) { + foundMachineVersion := 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 + } + } + + foundMachineVersion = true + latestVersion = version.Version + } + if !foundMachineVersion { + return nil, fmt.Errorf("no supported machine version found") + } + return latestVersion, nil } func toHibernationsPayload(ctx context.Context, m *Model) (*ske.Hibernation, error) { @@ -1020,6 +1224,25 @@ func mapFields(ctx context.Context, cl *ske.Cluster, m *Model) error { } func mapNodePools(ctx context.Context, cl *ske.Cluster, m *Model) error { + modelNodePoolOSVersion := map[string]basetypes.StringValue{} + modelNodePoolOSVersionMin := map[string]basetypes.StringValue{} + + modelNodePools := []nodePool{} + if !m.NodePools.IsNull() && !m.NodePools.IsUnknown() { + diags := m.NodePools.ElementsAs(ctx, &modelNodePools, false) + if diags.HasError() { + return core.DiagsToError(diags) + } + } + + for i := range modelNodePools { + name := conversion.StringValueToPointer(modelNodePools[i].Name) + if name != nil { + modelNodePoolOSVersion[*name] = modelNodePools[i].OSVersion + modelNodePoolOSVersionMin[*name] = modelNodePools[i].OSVersionMin + } + } + if cl.Nodepools == nil { m.NodePools = types.ListNull(types.ObjectType{AttrTypes: nodePoolTypes}) return nil @@ -1031,7 +1254,8 @@ func mapNodePools(ctx context.Context, cl *ske.Cluster, m *Model) error { "name": types.StringPointerValue(nodePoolResp.Name), "machine_type": types.StringPointerValue(nodePoolResp.Machine.Type), "os_name": types.StringNull(), - "os_version": types.StringNull(), + "os_version_min": modelNodePoolOSVersionMin[*nodePoolResp.Name], + "os_version": modelNodePoolOSVersion[*nodePoolResp.Name], "minimum": types.Int64PointerValue(nodePoolResp.Minimum), "maximum": types.Int64PointerValue(nodePoolResp.Maximum), "max_surge": types.Int64PointerValue(nodePoolResp.MaxSurge), @@ -1045,7 +1269,7 @@ func mapNodePools(ctx context.Context, cl *ske.Cluster, m *Model) error { if nodePoolResp.Machine != nil && nodePoolResp.Machine.Image != nil { nodePool["os_name"] = types.StringPointerValue(nodePoolResp.Machine.Image.Name) - nodePool["os_version"] = types.StringPointerValue(nodePoolResp.Machine.Image.Version) + nodePool["os_version_used"] = types.StringPointerValue(nodePoolResp.Machine.Image.Version) } if nodePoolResp.Volume != nil { @@ -1388,11 +1612,10 @@ func toKubernetesPayload(m *Model, availableVersions []ske.KubernetesVersion, cu // 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) + versionUsed, hasDeprecatedVersion, err := latestMatchingKubernetesVersion(availableVersions, providedVersionMin, currentKubernetesVersion) if err != nil { return nil, false, fmt.Errorf("getting latest matching kubernetes version: %w", err) } @@ -1404,7 +1627,7 @@ func toKubernetesPayload(m *Model, availableVersions []ske.KubernetesVersion, cu return k, hasDeprecatedVersion, nil } -func latestMatchingVersion(availableVersions []ske.KubernetesVersion, kubernetesVersionMin, currentKubernetesVersion *string) (version *string, deprecated bool, err error) { +func latestMatchingKubernetesVersion(availableVersions []ske.KubernetesVersion, kubernetesVersionMin, currentKubernetesVersion *string) (version *string, deprecated bool, err error) { deprecated = false if availableVersions == nil { @@ -1466,9 +1689,10 @@ func latestMatchingVersion(availableVersions []ske.KubernetesVersion, kubernetes break } } else { - // [MAJOR].[MINOR] version provided, get the latest patch version + // [MAJOR].[MINOR] version provided, get the latest non-preview patch version if semver.MajorMinor(vPreffixed) == semver.MajorMinor(providedVersionPrefixed) && - (semver.Compare(vPreffixed, providedVersionPrefixed) == 1) || (semver.Compare(vPreffixed, providedVersionPrefixed) == 0) { + (semver.Compare(vPreffixed, providedVersionPrefixed) == 1 || semver.Compare(vPreffixed, providedVersionPrefixed) == 0) && + (v.State != nil && *v.State != VersionStatePreview) { versionUsed = v.Version state = v.State } @@ -1476,11 +1700,7 @@ func latestMatchingVersion(availableVersions []ske.KubernetesVersion, kubernetes } if versionUsed != nil { - if strings.EqualFold(*state, VersionStateDeprecated) { - deprecated = true - } else { - deprecated = false - } + deprecated = strings.EqualFold(*state, VersionStateDeprecated) } // Throwing error if we could not match the version with the available versions @@ -1585,15 +1805,15 @@ func (r *clusterResource) Update(ctx context.Context, req resource.UpdateRequest ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "name", clName) - availableVersions, err := r.loadAvailableVersions(ctx) + availableKubernetesVersions, availableMachines, err := r.loadAvailableVersions(ctx) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating cluster", fmt.Sprintf("Loading available Kubernetes versions: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating cluster", fmt.Sprintf("Loading available Kubernetes and machine image versions: %v", err)) return } - currentKubernetesVersion := getCurrentKubernetesVersion(ctx, r.client, &model) + currentKubernetesVersion, currentMachineImages := getCurrentVersions(ctx, r.client, &model) - r.createOrUpdateCluster(ctx, &resp.Diagnostics, &model, availableVersions, currentKubernetesVersion) + r.createOrUpdateCluster(ctx, &resp.Diagnostics, &model, availableKubernetesVersions, availableMachines, currentKubernetesVersion, currentMachineImages) 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 93eb6cba..959a5d95 100644 --- a/stackit/internal/services/ske/cluster/resource_test.go +++ b/stackit/internal/services/ske/cluster/resource_test.go @@ -148,7 +148,9 @@ func TestMapFields(t *testing.T) { "name": types.StringValue("node"), "machine_type": types.StringValue("B"), "os_name": types.StringValue("os"), - "os_version": types.StringValue("os-ver"), + "os_version": types.StringNull(), + "os_version_min": types.StringNull(), + "os_version_used": types.StringValue("os-ver"), "minimum": types.Int64Value(1), "maximum": types.Int64Value(5), "max_surge": types.Int64Value(3), @@ -406,7 +408,7 @@ func TestMapFields(t *testing.T) { } } -func TestLatestMatchingVersion(t *testing.T) { +func TestLatestMatchingKubernetesVersion(t *testing.T) { tests := []struct { description string availableVersions []ske.KubernetesVersion @@ -494,6 +496,32 @@ func TestLatestMatchingVersion(t *testing.T) { false, true, }, + { + "available_version_with_higher_preview_patch_not_selected", + []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(VersionStatePreview), + }, + { + Version: utils.Ptr("1.19.0"), + State: utils.Ptr(VersionStateSupported), + }, + }, + utils.Ptr("1.20"), + nil, + utils.Ptr("1.20.1"), + false, + true, + }, { "available_version_no_provided_patch_2", []ske.KubernetesVersion{ @@ -530,24 +558,6 @@ func TestLatestMatchingVersion(t *testing.T) { true, true, }, - { - "deprecated_version_not_selected", - []ske.KubernetesVersion{ - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.19.0"), - State: utils.Ptr(VersionStateDeprecated), - }, - }, - utils.Ptr("1.20"), - nil, - utils.Ptr("1.20.0"), - false, - true, - }, { "preview_version", []ske.KubernetesVersion{ @@ -560,7 +570,7 @@ func TestLatestMatchingVersion(t *testing.T) { State: utils.Ptr(VersionStateSupported), }, }, - utils.Ptr("1.20"), + utils.Ptr("1.20.0"), nil, utils.Ptr("1.20.0"), false, @@ -678,42 +688,6 @@ func TestLatestMatchingVersion(t *testing.T) { 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{ @@ -777,7 +751,7 @@ func TestLatestMatchingVersion(t *testing.T) { false, }, { - "no_matching_available_versions_patch", + "no_matching_available_versions_patch_current", []ske.KubernetesVersion{ { Version: utils.Ptr("1.21.0"), @@ -799,7 +773,7 @@ func TestLatestMatchingVersion(t *testing.T) { false, }, { - "no_matching_available_versions_patch_2", + "no_matching_available_versions_patch_2_current", []ske.KubernetesVersion{ { Version: utils.Ptr("1.21.2"), @@ -859,7 +833,7 @@ func TestLatestMatchingVersion(t *testing.T) { } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - versionUsed, hasDeprecatedVersion, err := latestMatchingVersion(tt.availableVersions, tt.kubernetesVersionMin, tt.currentKubernetesVersion) + versionUsed, hasDeprecatedVersion, err := latestMatchingKubernetesVersion(tt.availableVersions, tt.kubernetesVersionMin, tt.currentKubernetesVersion) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } @@ -877,6 +851,517 @@ func TestLatestMatchingVersion(t *testing.T) { }) } } + +func TestLatestMatchingMachineVersion(t *testing.T) { + tests := []struct { + description string + availableVersions []ske.MachineImage + machineVersionMin *string + machineName string + currentMachineImage *ske.Image + expectedVersionUsed *string + expectedHasDeprecatedVersion bool + isValid bool + }{ + { + "available_version", + []ske.MachineImage{ + { + Name: utils.Ptr("foo"), + Versions: &[]ske.MachineImageVersion{ + { + 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), + }, + }, + }, + }, + utils.Ptr("1.20.1"), + "foo", + nil, + utils.Ptr("1.20.1"), + false, + true, + }, + { + "available_version_zero_patch", + []ske.MachineImage{ + { + Name: utils.Ptr("foo"), + Versions: &[]ske.MachineImageVersion{ + { + 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), + }, + }, + }, + }, + utils.Ptr("1.20.0"), + "foo", + nil, + utils.Ptr("1.20.0"), + false, + true, + }, + { + "available_version_with_no_provided_patch", + []ske.MachineImage{ + { + Name: utils.Ptr("foo"), + Versions: &[]ske.MachineImageVersion{ + { + 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), + }, + }, + }, + }, + utils.Ptr("1.20"), + "foo", + nil, + utils.Ptr("1.20.2"), + false, + true, + }, + { + "available_version_with_higher_preview_patch_not_selected", + []ske.MachineImage{ + { + Name: utils.Ptr("foo"), + Versions: &[]ske.MachineImageVersion{ + { + 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(VersionStatePreview), + }, + { + Version: utils.Ptr("1.19.0"), + State: utils.Ptr(VersionStateSupported), + }, + }, + }, + }, + utils.Ptr("1.20"), + "foo", + nil, + utils.Ptr("1.20.1"), + false, + true, + }, + { + "available_version_with_no_provided_patch_2", + []ske.MachineImage{ + { + Name: utils.Ptr("foo"), + Versions: &[]ske.MachineImageVersion{ + { + Version: utils.Ptr("1.20.0"), + State: utils.Ptr(VersionStateSupported), + }, + { + Version: utils.Ptr("1.19.0"), + State: utils.Ptr(VersionStateSupported), + }, + }, + }, + }, + utils.Ptr("1.20"), + "foo", + nil, + utils.Ptr("1.20.0"), + false, + true, + }, + { + "deprecated_version", + []ske.MachineImage{ + { + Name: utils.Ptr("foo"), + Versions: &[]ske.MachineImageVersion{ + { + Version: utils.Ptr("1.20.0"), + State: utils.Ptr(VersionStateSupported), + }, + { + Version: utils.Ptr("1.19.0"), + State: utils.Ptr(VersionStateDeprecated), + }, + }, + }, + }, + utils.Ptr("1.19"), + "foo", + nil, + utils.Ptr("1.19.0"), + true, + true, + }, + { + "preview_version_selected", + []ske.MachineImage{ + { + Name: utils.Ptr("foo"), + Versions: &[]ske.MachineImageVersion{ + { + Version: utils.Ptr("1.20.0"), + State: utils.Ptr(VersionStatePreview), + }, + { + Version: utils.Ptr("1.19.0"), + State: utils.Ptr(VersionStateDeprecated), + }, + }, + }, + }, + utils.Ptr("1.20.0"), + "foo", + nil, + utils.Ptr("1.20.0"), + false, + true, + }, + { + "nil_provided_version_get_latest", + []ske.MachineImage{ + { + Name: utils.Ptr("foo"), + Versions: &[]ske.MachineImageVersion{ + { + Version: utils.Ptr("1.20.0"), + State: utils.Ptr(VersionStateSupported), + }, + { + Version: utils.Ptr("1.19.0"), + State: utils.Ptr(VersionStateSupported), + }, + }, + }, + }, + nil, + "foo", + nil, + utils.Ptr("1.20.0"), + false, + true, + }, + { + "nil_provided_version_use_current", + []ske.MachineImage{ + { + Name: utils.Ptr("foo"), + Versions: &[]ske.MachineImageVersion{ + { + Version: utils.Ptr("1.20.0"), + State: utils.Ptr(VersionStateSupported), + }, + { + Version: utils.Ptr("1.19.0"), + State: utils.Ptr(VersionStateSupported), + }, + }, + }, + }, + nil, + "foo", + &ske.Image{ + Name: utils.Ptr("foo"), + Version: utils.Ptr("1.19.0"), + }, + utils.Ptr("1.19.0"), + false, + true, + }, + { + "nil_provided_version_os_image_update_get_latest", + []ske.MachineImage{ + { + Name: utils.Ptr("foo"), + Versions: &[]ske.MachineImageVersion{ + { + Version: utils.Ptr("1.20.0"), + State: utils.Ptr(VersionStateSupported), + }, + { + Version: utils.Ptr("1.19.0"), + State: utils.Ptr(VersionStateSupported), + }, + }, + }, + }, + nil, + "foo", + &ske.Image{ + Name: utils.Ptr("bar"), + Version: utils.Ptr("1.19.0"), + }, + utils.Ptr("1.20.0"), + false, + true, + }, + { + "update_lower_min_provided", + []ske.MachineImage{ + { + Name: utils.Ptr("foo"), + Versions: &[]ske.MachineImageVersion{ + { + Version: utils.Ptr("1.20.0"), + State: utils.Ptr(VersionStateSupported), + }, + { + Version: utils.Ptr("1.19.0"), + State: utils.Ptr(VersionStateSupported), + }, + }, + }, + }, + utils.Ptr("1.19"), + "foo", + &ske.Image{ + Name: utils.Ptr("foo"), + Version: utils.Ptr("1.20.0"), + }, + utils.Ptr("1.20.0"), + false, + true, + }, + { + "update_lower_min_provided_deprecated_version", + []ske.MachineImage{ + { + Name: utils.Ptr("foo"), + Versions: &[]ske.MachineImageVersion{ + { + 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(VersionStateSupported), + }, + }, + }, + }, + utils.Ptr("1.19"), + "foo", + &ske.Image{ + Name: utils.Ptr("foo"), + Version: utils.Ptr("1.20.0"), + }, + utils.Ptr("1.20.0"), + true, + true, + }, + { + "update_higher_min_provided", + []ske.MachineImage{ + { + Name: utils.Ptr("foo"), + Versions: &[]ske.MachineImageVersion{ + { + Version: utils.Ptr("1.20.0"), + State: utils.Ptr(VersionStateSupported), + }, + { + Version: utils.Ptr("1.19.0"), + State: utils.Ptr(VersionStateSupported), + }, + }, + }, + }, + utils.Ptr("1.20"), + "foo", + &ske.Image{ + Name: utils.Ptr("foo"), + Version: utils.Ptr("1.19.0"), + }, + utils.Ptr("1.20.0"), + false, + true, + }, + { + "no_matching_available_versions", + []ske.MachineImage{ + { + Name: utils.Ptr("foo"), + Versions: &[]ske.MachineImageVersion{ + { + Version: utils.Ptr("1.20.0"), + State: utils.Ptr(VersionStateSupported), + }, + { + Version: utils.Ptr("1.19.0"), + State: utils.Ptr(VersionStateSupported), + }, + }, + }, + }, + utils.Ptr("1.21"), + "foo", + nil, + nil, + false, + false, + }, + { + "no_available_versions", + []ske.MachineImage{ + { + Name: utils.Ptr("foo"), + Versions: &[]ske.MachineImageVersion{}, + }, + }, + utils.Ptr("1.20"), + "foo", + nil, + nil, + false, + false, + }, + { + "nil_available_versions", + []ske.MachineImage{ + { + Name: utils.Ptr("foo"), + Versions: nil, + }, + }, + utils.Ptr("1.20"), + "foo", + nil, + nil, + false, + false, + }, + { + "nil_name", + []ske.MachineImage{ + { + Name: nil, + Versions: &[]ske.MachineImageVersion{ + { + Version: utils.Ptr("1.20.0"), + State: utils.Ptr(VersionStateSupported), + }, + }, + }, + }, + utils.Ptr("1.20"), + "foo", + nil, + nil, + false, + false, + }, + { + "name_not_available", + []ske.MachineImage{ + { + Name: utils.Ptr("bar"), + Versions: &[]ske.MachineImageVersion{ + { + Version: utils.Ptr("1.20.0"), + State: utils.Ptr(VersionStateSupported), + }, + }, + }, + }, + utils.Ptr("1.20"), + "foo", + nil, + nil, + false, + false, + }, + { + "empty_provided_version", + []ske.MachineImage{ + { + Name: utils.Ptr("foo"), + Versions: &[]ske.MachineImageVersion{ + { + Version: utils.Ptr("1.20.0"), + State: utils.Ptr(VersionStateSupported), + }, + }, + }, + }, + utils.Ptr(""), + "foo", + nil, + nil, + false, + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + versionUsed, hasDeprecatedVersion, err := latestMatchingMachineVersion(tt.availableVersions, tt.machineVersionMin, tt.machineName, tt.currentMachineImage) + if !tt.isValid && err == nil { + t.Fatalf("Should have failed") + } + if tt.isValid && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + if tt.isValid { + if *versionUsed != *tt.expectedVersionUsed { + t.Fatalf("Used version does not match: expecting %s, got %s", *tt.expectedVersionUsed, *versionUsed) + } + if tt.expectedHasDeprecatedVersion != hasDeprecatedVersion { + t.Fatalf("hasDeprecatedVersion flag is wrong: expecting %t, got %t", tt.expectedHasDeprecatedVersion, hasDeprecatedVersion) + } + } + }) + } +} + func TestGetMaintenanceTimes(t *testing.T) { tests := []struct { description string @@ -1111,12 +1596,13 @@ func TestCheckAllowPrivilegedContainers(t *testing.T) { } } -func TestGetCurrentKubernetesVersion(t *testing.T) { +func TestGetCurrentVersion(t *testing.T) { tests := []struct { - description string - mockedResp *ske.Cluster - expected *string - getClusterFails bool + description string + mockedResp *ske.Cluster + expectedKubernetesVersion *string + expectedMachineImages map[string]*ske.Image + getClusterFails bool }{ { "ok", @@ -1124,25 +1610,46 @@ func TestGetCurrentKubernetesVersion(t *testing.T) { Kubernetes: &ske.Kubernetes{ Version: utils.Ptr("v1.0.0"), }, + Nodepools: &[]ske.Nodepool{ + { + Name: utils.Ptr("foo"), + Machine: &ske.Machine{ + Image: &ske.Image{ + Name: utils.Ptr("foo"), + Version: utils.Ptr("v1.0.0"), + }, + }, + }, + { + Name: utils.Ptr("bar"), + Machine: &ske.Machine{ + Image: &ske.Image{ + Name: utils.Ptr("bar"), + Version: utils.Ptr("v2.0.0"), + }, + }, + }, + }, }, utils.Ptr("v1.0.0"), + map[string]*ske.Image{ + "foo": { + Name: utils.Ptr("foo"), + Version: utils.Ptr("v1.0.0"), + }, + "bar": { + Name: utils.Ptr("bar"), + Version: utils.Ptr("v2.0.0"), + }, + }, false, }, { "get fails", nil, nil, - true, - }, - { - "nil version", - &ske.Cluster{ - Kubernetes: &ske.Kubernetes{ - Version: nil, - }, - }, nil, - false, + true, }, { "nil kubernetes", @@ -1150,12 +1657,121 @@ func TestGetCurrentKubernetesVersion(t *testing.T) { Kubernetes: nil, }, nil, + nil, + false, + }, + { + "nil kubernetes version", + &ske.Cluster{ + Kubernetes: &ske.Kubernetes{ + Version: nil, + }, + }, + nil, + nil, + false, + }, + { + "nil nodepools", + &ske.Cluster{ + Kubernetes: &ske.Kubernetes{ + Version: utils.Ptr("v1.0.0"), + }, + Nodepools: nil, + }, + utils.Ptr("v1.0.0"), + nil, + false, + }, + { + "nil nodepools machine", + &ske.Cluster{ + Kubernetes: &ske.Kubernetes{ + Version: utils.Ptr("v1.0.0"), + }, + Nodepools: &[]ske.Nodepool{ + { + Name: utils.Ptr("foo"), + Machine: nil, + }, + }, + }, + utils.Ptr("v1.0.0"), + map[string]*ske.Image{}, + false, + }, + { + "nil nodepools machine image", + &ske.Cluster{ + Kubernetes: &ske.Kubernetes{ + Version: utils.Ptr("v1.0.0"), + }, + Nodepools: &[]ske.Nodepool{ + { + Name: utils.Ptr("foo"), + Machine: &ske.Machine{ + Image: nil, + }, + }, + }, + }, + utils.Ptr("v1.0.0"), + map[string]*ske.Image{}, + false, + }, + { + "nil nodepools machine image name", + &ske.Cluster{ + Kubernetes: &ske.Kubernetes{ + Version: utils.Ptr("v1.0.0"), + }, + Nodepools: &[]ske.Nodepool{ + { + Name: utils.Ptr("foo"), + Machine: &ske.Machine{ + Image: &ske.Image{ + Name: nil, + }, + }, + }, + }, + }, + utils.Ptr("v1.0.0"), + map[string]*ske.Image{}, + false, + }, + { + "nil nodepools machine image version", + &ske.Cluster{ + Kubernetes: &ske.Kubernetes{ + Version: utils.Ptr("v1.0.0"), + }, + Nodepools: &[]ske.Nodepool{ + { + Name: utils.Ptr("foo"), + Machine: &ske.Machine{ + Image: &ske.Image{ + Name: utils.Ptr("foo"), + Version: nil, + }, + }, + }, + }, + }, + utils.Ptr("v1.0.0"), + map[string]*ske.Image{ + "foo": { + Name: utils.Ptr("foo"), + Version: nil, + }, + }, false, }, { "nil response", nil, nil, + nil, false, }, } @@ -1169,16 +1785,21 @@ func TestGetCurrentKubernetesVersion(t *testing.T) { ProjectId: types.StringValue("pid"), Name: types.StringValue("name"), } - version := getCurrentKubernetesVersion(context.Background(), client, model) - diff := cmp.Diff(version, tt.expected) + kubernetesVersion, machineImageVersions := getCurrentVersions(context.Background(), client, model) + diff := cmp.Diff(kubernetesVersion, tt.expectedKubernetesVersion) if diff != "" { - t.Fatalf("Version does not match: %s", diff) + t.Errorf("Kubernetes version does not match: %s", diff) + } + + diff = cmp.Diff(machineImageVersions, tt.expectedMachineImages) + if diff != "" { + t.Errorf("Machine images do not match: %s", diff) } }) } } -func TestGetLatestSupportedVersion(t *testing.T) { +func TestGetLatestSupportedKubernetesVersion(t *testing.T) { tests := []struct { description string listKubernetesVersion []ske.KubernetesVersion @@ -1246,3 +1867,72 @@ func TestGetLatestSupportedVersion(t *testing.T) { }) } } + +func TestGetLatestSupportedMachineVersion(t *testing.T) { + tests := []struct { + description string + listMachineVersion []ske.MachineImageVersion + isValid bool + expectedVersion *string + }{ + { + description: "base", + listMachineVersion: []ske.MachineImageVersion{ + { + 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 mchine versions 1", + listMachineVersion: nil, + isValid: false, + }, + { + description: "no machine versions 2", + listMachineVersion: []ske.MachineImageVersion{}, + isValid: false, + }, + { + description: "no supported machine versions", + listMachineVersion: []ske.MachineImageVersion{ + { + 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 := getLatestSupportedMachineVersion(tt.listMachineVersion) + + 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 cfd3adba..608dcc2e 100644 --- a/stackit/internal/services/ske/ske_acc_test.go +++ b/stackit/internal/services/ske/ske_acc_test.go @@ -28,8 +28,10 @@ var clusterResource = map[string]string{ "nodepool_name": "np-acc-test", "nodepool_name_min": "np-acc-min-test", "nodepool_machine_type": "b1.2", - "nodepool_os_version": "3815.2.1", - "nodepool_os_version_min": "3815.2.1", + "nodepool_os_version_min": "3815.2", + "nodepool_os_version_used": "3815.2.1", + "nodepool_os_version_min_new": "3815.2.1", + "nodepool_os_version_used_new": "3815.2.1", "nodepool_os_name": "flatcar", "nodepool_minimum": "2", "nodepool_maximum": "3", @@ -59,7 +61,7 @@ var clusterResource = map[string]string{ "kubeconfig_expiration": "3600", } -func getConfig(version string, maintenanceEnd *string) string { +func getConfig(kubernetesVersion, nodePoolMachineOSVersion string, maintenanceEnd *string) string { maintenanceEndTF := clusterResource["maintenance_end"] if maintenanceEnd != nil { maintenanceEndTF = *maintenanceEnd @@ -79,7 +81,7 @@ func getConfig(version string, maintenanceEnd *string) string { max_surge = "%s" max_unavailable = "%s" os_name = "%s" - os_version = "%s" + os_version_min = "%s" volume_size = "%s" volume_type = "%s" cri = "%s" @@ -128,7 +130,6 @@ func getConfig(version string, maintenanceEnd *string) string { node_pools = [{ name = "%s" machine_type = "%s" - os_version = "%s" minimum = "%s" maximum = "%s" availability_zones = ["%s"] @@ -144,7 +145,7 @@ func getConfig(version string, maintenanceEnd *string) string { testutil.SKEProviderConfig(), clusterResource["project_id"], clusterResource["name"], - version, + kubernetesVersion, clusterResource["nodepool_name"], clusterResource["nodepool_machine_type"], clusterResource["nodepool_minimum"], @@ -152,7 +153,7 @@ func getConfig(version string, maintenanceEnd *string) string { clusterResource["nodepool_max_surge"], clusterResource["nodepool_max_unavailable"], clusterResource["nodepool_os_name"], - clusterResource["nodepool_os_version"], + nodePoolMachineOSVersion, clusterResource["nodepool_volume_size"], clusterResource["nodepool_volume_type"], clusterResource["nodepool_cri"], @@ -182,7 +183,6 @@ func getConfig(version string, maintenanceEnd *string) string { clusterResource["name_min"], clusterResource["nodepool_name_min"], clusterResource["nodepool_machine_type"], - clusterResource["nodepool_os_version_min"], clusterResource["nodepool_minimum"], clusterResource["nodepool_maximum"], clusterResource["nodepool_zone"], @@ -201,7 +201,7 @@ func TestAccSKE(t *testing.T) { // 1) Creation { - Config: getConfig(clusterResource["kubernetes_version_min"], nil), + Config: getConfig(clusterResource["kubernetes_version_min"], clusterResource["nodepool_os_version_min"], nil), Check: resource.ComposeAggregateTestCheckFunc( // cluster data resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "name", clusterResource["name"]), @@ -211,7 +211,8 @@ func TestAccSKE(t *testing.T) { resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.availability_zones.#", "1"), resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.availability_zones.0", clusterResource["nodepool_zone"]), resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.os_name", clusterResource["nodepool_os_name"]), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.os_version", clusterResource["nodepool_os_version"]), + resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.os_version_min", clusterResource["nodepool_os_version_min"]), + resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.os_version_used", clusterResource["nodepool_os_version_used"]), resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.machine_type", clusterResource["nodepool_machine_type"]), resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.minimum", clusterResource["nodepool_minimum"]), resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.maximum", clusterResource["nodepool_maximum"]), @@ -262,7 +263,7 @@ func TestAccSKE(t *testing.T) { resource.TestCheckResourceAttr("stackit_ske_cluster.cluster_min", "node_pools.0.availability_zones.#", "1"), resource.TestCheckResourceAttr("stackit_ske_cluster.cluster_min", "node_pools.0.availability_zones.0", clusterResource["nodepool_zone"]), resource.TestCheckResourceAttrSet("stackit_ske_cluster.cluster_min", "node_pools.0.os_name"), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster_min", "node_pools.0.os_version", clusterResource["nodepool_os_version_min"]), + resource.TestCheckResourceAttr("stackit_ske_cluster.cluster_min", "node_pools.0.os_version_used", clusterResource["nodepool_os_version_used"]), resource.TestCheckResourceAttr("stackit_ske_cluster.cluster_min", "node_pools.0.machine_type", clusterResource["nodepool_machine_type"]), resource.TestCheckResourceAttr("stackit_ske_cluster.cluster_min", "node_pools.0.minimum", clusterResource["nodepool_minimum"]), resource.TestCheckResourceAttr("stackit_ske_cluster.cluster_min", "node_pools.0.maximum", clusterResource["nodepool_maximum"]), @@ -300,7 +301,7 @@ func TestAccSKE(t *testing.T) { } `, - getConfig(clusterResource["kubernetes_version_min"], nil), + getConfig(clusterResource["kubernetes_version_min"], clusterResource["nodepool_os_version_min"], nil), clusterResource["project_id"], clusterResource["name"], clusterResource["project_id"], @@ -319,7 +320,6 @@ func TestAccSKE(t *testing.T) { resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.availability_zones.#", "1"), resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.availability_zones.0", clusterResource["nodepool_zone"]), resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.os_name", clusterResource["nodepool_os_name"]), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.os_version", clusterResource["nodepool_os_version"]), resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.machine_type", clusterResource["nodepool_machine_type"]), resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.minimum", clusterResource["nodepool_minimum"]), resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.maximum", clusterResource["nodepool_maximum"]), @@ -356,7 +356,6 @@ func TestAccSKE(t *testing.T) { resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster_min", "node_pools.0.availability_zones.#", "1"), resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster_min", "node_pools.0.availability_zones.0", clusterResource["nodepool_zone"]), resource.TestCheckResourceAttrSet("data.stackit_ske_cluster.cluster_min", "node_pools.0.os_name"), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster_min", "node_pools.0.os_version", clusterResource["nodepool_os_version_min"]), resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster_min", "node_pools.0.machine_type", clusterResource["nodepool_machine_type"]), resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster_min", "node_pools.0.minimum", clusterResource["nodepool_minimum"]), resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster_min", "node_pools.0.maximum", clusterResource["nodepool_maximum"]), @@ -397,7 +396,7 @@ 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{"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.%"}, + ImportStateVerifyIgnore: []string{"kubernetes_version_min", "kube_config", "node_pools.0.os_version_min", "extensions.argus.%", "extensions.argus.argus_instance_id", "extensions.argus.enabled", "extensions.acl.enabled", "extensions.acl.allowed_cidrs", "extensions.acl.allowed_cidrs.#", "extensions.acl.%"}, }, // 4) Import minimal cluster { @@ -419,11 +418,11 @@ func TestAccSKE(t *testing.T) { }, ImportState: true, ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"kubernetes_version_min", "kube_config"}, + ImportStateVerifyIgnore: []string{"kubernetes_version_min", "kube_config", "node_pools.0.os_version_min"}, }, - // 5) Update kubernetes version and maximum + // 5) Update kubernetes version, OS version and maintenance end { - Config: getConfig(clusterResource["kubernetes_version_min_new"], utils.Ptr(clusterResource["maintenance_end_new"])), + Config: getConfig(clusterResource["kubernetes_version_min_new"], clusterResource["os_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"]), @@ -434,7 +433,8 @@ func TestAccSKE(t *testing.T) { resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.availability_zones.#", "1"), resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.availability_zones.0", clusterResource["nodepool_zone"]), resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.os_name", clusterResource["nodepool_os_name"]), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.os_version", clusterResource["nodepool_os_version"]), + resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.os_version_min", clusterResource["nodepool_os_version_min_new"]), + resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.os_version_used", clusterResource["nodepool_os_version_used_new"]), resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.machine_type", clusterResource["nodepool_machine_type"]), resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.minimum", clusterResource["nodepool_minimum"]), resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.maximum", clusterResource["nodepool_maximum"]), @@ -465,15 +465,17 @@ func TestAccSKE(t *testing.T) { resource.TestCheckNoResourceAttr("stackit_ske_cluster.cluster", "kube_config"), // when using the kubeconfig resource, the kubeconfig field becomes null ), }, - // 6) Downgrade kubernetes version + // 6) Downgrade kubernetes and nodepool machine OS version { - Config: getConfig(clusterResource["kubernetes_version_min"], utils.Ptr(clusterResource["maintenance_end_new"])), + Config: getConfig(clusterResource["kubernetes_version_min"], clusterResource["nodepool_os_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_min", clusterResource["kubernetes_version_min"]), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "kubernetes_version_used", clusterResource["kubernetes_version_used"]), + resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "kubernetes_version_used", clusterResource["kubernetes_version_used_new"]), + resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.os_version_min", clusterResource["nodepool_os_version_min"]), + resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.os_version_used", clusterResource["nodepool_os_version_used_new"]), ), }, // Deletion is done by the framework implicitly