From aab0fb8a80ae1dd002ba1ebca4ade3f01fae88e5 Mon Sep 17 00:00:00 2001 From: fabiante Date: Tue, 2 Dec 2025 12:14:33 +0100 Subject: [PATCH 1/7] Add support for pool taints --- gridscale/resource_gridscale_k8s.go | 181 +++++++++++++++++++++++ gridscale/resource_gridscale_k8s_test.go | 113 ++++++++++++++ website/docs/r/k8s.html.md | 32 ++++ 3 files changed, 326 insertions(+) diff --git a/gridscale/resource_gridscale_k8s.go b/gridscale/resource_gridscale_k8s.go index 156ebc44..cf92cccd 100644 --- a/gridscale/resource_gridscale_k8s.go +++ b/gridscale/resource_gridscale_k8s.go @@ -25,6 +25,7 @@ const ( k8sLabelPrefix = "#gsk#" k8sRocketStorageSupportRelease = "1.26" k8sMultiNodePoolSupportRelease = "1.30" + k8sTaintKeyValueRegex = `^[a-zA-Z0-9-]+$` ) // ResourceGridscaleK8sModeler struct represents a modeler of the gridscale k8s resource. @@ -117,6 +118,33 @@ func (rgk8sm *ResourceGridscaleK8sModeler) buildInputSchema() map[string]*schema Default: 0, Description: "Rocket storage per worker node (in GiB).", }, + "taints": { + Type: schema.TypeList, + Optional: true, + Description: "List of taints to be applied to the nodes of this pool.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Required: true, + Description: "The key of the taint.", + ValidateFunc: validation.StringMatch(regexp.MustCompile(k8sTaintKeyValueRegex), "key must consist of alphanumeric characters and hyphens only"), + }, + "value": { + Type: schema.TypeString, + Required: true, + Description: "The value of the taint.", + ValidateFunc: validation.StringMatch(regexp.MustCompile(k8sTaintKeyValueRegex), "value must consist of alphanumeric characters and hyphens only"), + }, + "effect": { + Type: schema.TypeString, + Required: true, + Description: "The effect of the taint.", + ValidateFunc: validation.StringInSlice([]string{"NoExecute", "NoSchedule", "PreferNoSchedule"}, false), + }, + }, + }, + }, } return map[string]*schema.Schema{ "name": { @@ -852,6 +880,32 @@ func resourceGridscaleK8sRead(d *schema.ResourceData, meta interface{}) error { if rocketStorage, isRocketStorageSet := nodePoolSet["rocket_storage"]; isRocketStorageSet { nodePoolRead["rocket_storage"] = rocketStorage } + + // Handle taints + if taints, isTaintsSet := nodePoolSet["taints"]; isTaintsSet { + taintsList := taints.([]any) + taintsRead := make([]map[string]any, 0) + + for _, taintInterface := range taintsList { + taint := taintInterface.(map[string]any) + taintRead := make(map[string]any) + + if key, isKeySet := taint["key"]; isKeySet { + taintRead["key"] = key + } + if value, isValueSet := taint["value"]; isValueSet { + taintRead["value"] = value + } + if effect, isEffectSet := taint["effect"]; isEffectSet { + taintRead["effect"] = effect + } + + taintsRead = append(taintsRead, taintRead) + } + + nodePoolRead["taints"] = taintsRead + } + nodePools = append(nodePools, nodePoolRead) } } @@ -952,6 +1006,31 @@ func resourceGridscaleK8sCreate(d *schema.ResourceData, meta interface{}) error nodePool["storage_type"] = d.Get(fmt.Sprintf("node_pool.%d.storage_type", index)) nodePool["rocket_storage"] = d.Get(fmt.Sprintf("node_pool.%d.rocket_storage", index)) + // Handle taints + if taintsInterface, isTaintsSet := d.GetOk(fmt.Sprintf("node_pool.%d.taints", index)); isTaintsSet { + taintsList := taintsInterface.([]any) + taintsRequest := make([]map[string]any, 0) + + for _, taintInterface := range taintsList { + taint := taintInterface.(map[string]any) + taintRequest := make(map[string]any) + + if key, isKeySet := taint["key"]; isKeySet { + taintRequest["key"] = key + } + if value, isValueSet := taint["value"]; isValueSet { + taintRequest["value"] = value + } + if effect, isEffectSet := taint["effect"]; isEffectSet { + taintRequest["effect"] = effect + } + + taintsRequest = append(taintsRequest, taintRequest) + } + + nodePool["taints"] = taintsRequest + } + nodePools = append(nodePools, nodePool) } parameters["pools"] = nodePools @@ -1113,6 +1192,31 @@ func resourceGridscaleK8sUpdate(d *schema.ResourceData, meta interface{}) error nodePool["storage_type"] = d.Get(fmt.Sprintf("node_pool.%d.storage_type", index)) nodePool["rocket_storage"] = d.Get(fmt.Sprintf("node_pool.%d.rocket_storage", index)) + // Handle taints + if taintsInterface, isTaintsSet := d.GetOk(fmt.Sprintf("node_pool.%d.taints", index)); isTaintsSet { + taintsList := taintsInterface.([]any) + taintsRequest := make([]map[string]any, 0) + + for _, taintInterface := range taintsList { + taint := taintInterface.(map[string]any) + taintRequest := make(map[string]any) + + if key, isKeySet := taint["key"]; isKeySet { + taintRequest["key"] = key + } + if value, isValueSet := taint["value"]; isValueSet { + taintRequest["value"] = value + } + if effect, isEffectSet := taint["effect"]; isEffectSet { + taintRequest["effect"] = effect + } + + taintsRequest = append(taintsRequest, taintRequest) + } + + nodePool["taints"] = taintsRequest + } + nodePools = append(nodePools, nodePool) } parameters["pools"] = nodePools @@ -1378,6 +1482,83 @@ func validateK8sParameters(d *schema.ResourceDiff, template gsclient.PaaSTemplat ) } } + + // Validate taints + nodePoolParameterTaints, taints_ok := templateParameterNodePools.Schema.Schema["taints"] + if taints_ok { + if taintsInterface, isTaintsSet := d.GetOk(fmt.Sprintf("node_pool.%d.taints", index)); isTaintsSet { + taintsList := taintsInterface.([]any) + + // Check if taints list is empty when it's allowed to be + if len(taintsList) == 0 && !nodePoolParameterTaints.Empty { + errorMessages = append( + errorMessages, + fmt.Sprintf("Invalid 'node_pool.%d.taints' value. Taints list cannot be empty.\n", index), + ) + } + + // Validate each taint + for _, taintInterface := range taintsList { + taint := taintInterface.(map[string]any) + + // Validate key + if key, isKeySet := taint["key"]; isKeySet { + keyStr := key.(string) + if !regexp.MustCompile(k8sTaintKeyValueRegex).MatchString(keyStr) { + errorMessages = append( + errorMessages, + fmt.Sprintf("Invalid 'node_pool.%d.taints.key' value. Key must consist of alphanumeric characters and hyphens only.\n", index), + ) + } + } else { + errorMessages = append( + errorMessages, + fmt.Sprintf("Invalid 'node_pool.%d.taints' value. Key is required.\n", index), + ) + } + + // Validate value + if value, isValueSet := taint["value"]; isValueSet { + valueStr := value.(string) + if !regexp.MustCompile(k8sTaintKeyValueRegex).MatchString(valueStr) { + errorMessages = append( + errorMessages, + fmt.Sprintf("Invalid 'node_pool.%d.taints.value' value. Value must consist of alphanumeric characters and hyphens only.\n", index), + ) + } + } else { + errorMessages = append( + errorMessages, + fmt.Sprintf("Invalid 'node_pool.%d.taints' value. Value is required.\n", index), + ) + } + + // Validate effect + if effect, isEffectSet := taint["effect"]; isEffectSet { + effectStr := effect.(string) + validEffects := []string{"NoExecute", "NoSchedule", "PreferNoSchedule"} + isValidEffect := false + for _, validEffect := range validEffects { + if effectStr == validEffect { + isValidEffect = true + break + } + } + if !isValidEffect { + errorMessages = append( + errorMessages, + fmt.Sprintf("Invalid 'node_pool.%d.taints.effect' value. Effect must be one of: %s.\n", index, strings.Join(validEffects, ", ")), + ) + } + } else { + errorMessages = append( + errorMessages, + fmt.Sprintf("Invalid 'node_pool.%d.taints' value. Effect is required.\n", index), + ) + } + } + } + } } } diff --git a/gridscale/resource_gridscale_k8s_test.go b/gridscale/resource_gridscale_k8s_test.go index 57e21ee8..0bd2ded7 100644 --- a/gridscale/resource_gridscale_k8s_test.go +++ b/gridscale/resource_gridscale_k8s_test.go @@ -101,6 +101,70 @@ func TestAccResourceGridscaleK8sBasic(t *testing.T) { "gridscale_k8s.foopaas", "surge_node", "true"), ), }, + { + Config: testAccCheckResourceGridscaleK8sConfigAddTaint(), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceGridscalePaaSExists("gridscale_k8s.foopaas", &object), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.name", "my-node-pool"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.node_count", "1"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.cores", "2"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.memory", "4"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.storage", "50"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.storage_type", "storage_insane"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.rocket_storage", "10"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.taints.#", "2"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.taints.0.key", "example-key"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.taints.0.value", "example-value"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.taints.0.effect", "NoSchedule"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.taints.1.key", "another-key"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.taints.1.value", "another-value"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.taints.1.effect", "NoExecute"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "k8s_hubble", "true"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "surge_node", "true"), + ), + }, + { + Config: testAccCheckResourceGridscaleK8sConfigRemoveTaint(), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceGridscalePaaSExists("gridscale_k8s.foopaas", &object), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.name", "my-node-pool"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.node_count", "1"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.cores", "2"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.memory", "4"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.storage", "50"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.storage_type", "storage_insane"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.rocket_storage", "10"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.taints.#", "0"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "k8s_hubble", "true"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "surge_node", "true"), + ), + }, { Config: testAccCheckResourceGridscaleK8sConfigNodeCountIncrease(), Check: resource.ComposeTestCheckFunc( @@ -292,6 +356,55 @@ func testAccCheckResourceGridscaleK8sConfigNodeCountIncrease() string { ` } +func testAccCheckResourceGridscaleK8sConfigAddTaint() string { + return ` + resource "gridscale_k8s" "foopaas" { + name = "newname" + release = "1.32" + node_pool { + name = "my-node-pool" + node_count = 1 + cores = 2 + memory = 4 + storage = 50 + storage_type = "storage_insane" + rocket_storage = 10 + taints { + key = "example-key" + value = "example-value" + effect = "NoSchedule" + } + taints { + key = "another-key" + value = "another-value" + effect = "NoExecute" + } + } + k8s_hubble = true + } + ` +} + +func testAccCheckResourceGridscaleK8sConfigRemoveTaint() string { + return ` + resource "gridscale_k8s" "foopaas" { + name = "newname" + release = "1.32" + node_pool { + name = "my-node-pool" + node_count = 1 + cores = 2 + memory = 4 + storage = 50 + storage_type = "storage_insane" + rocket_storage = 10 + taints = [] + } + k8s_hubble = true + } + ` +} + func testAccCheckResourceGridscaleK8sConfigNodeCountDecrease() string { return ` resource "gridscale_k8s" "foopaas" { diff --git a/website/docs/r/k8s.html.md b/website/docs/r/k8s.html.md index ea2cf9ab..1e77c147 100644 --- a/website/docs/r/k8s.html.md +++ b/website/docs/r/k8s.html.md @@ -62,6 +62,7 @@ The following arguments are supported: * `storage` - Storage per worker node (in GiB). * `storage_type` - Storage type (one of storage, storage_high, storage_insane). * `rocket_storage` - Rocket storage per worker node (in GiB). + * `taints` - List of taints to be applied to the nodes of this pool. * `surge_node` - Enable surge node to avoid resources shortage during the cluster upgrade (Default: true). * `cluster_cidr` - (Immutable) The cluster CIDR that will be used to generate the CIDR of nodes, services, and pods. The allowed CIDR prefix length is /16. If the cluster CIDR is not set, the cluster will use "10.244.0.0/16" as it default (even though the `cluster_cidr` in the k8s resource is empty). * `cluster_traffic_encryption` - Enables cluster encryption via wireguard if true. Only available for GSK version 1.29 and above. Default is false. @@ -88,6 +89,36 @@ The following arguments are supported: * `k8s_hubble` - (Optional) Enable Hubble for the k8s cluster. +### Examples + +#### Pool Taints + +```terraform +resource "gridscale_k8s" "k8s-test" { + name = "test" + release = "1.30" # instead, gsk_version can be set. + + node_pool { + name = "pool-0" + node_count = 2 + cores = 2 + memory = 4 + storage = 30 + storage_type = "storage_insane" + + taints { + key = "example-key" + value = "example-value" + effect = "NoSchedule" + } + taints { + key = "another-key" + value = "another-value" + effect = "NoExecute" + } + } +} +``` ## Timeouts @@ -120,6 +151,7 @@ This resource exports the following attributes: * `storage` - See Argument Reference above. * `storage_type` - See Argument Reference above. * `rocket_storage` - See Argument Reference above. + * `taints` - See Argument Reference above. * `surge_node` - See Argument Reference above. * `cluster_cidr` - See Argument Reference above. * `cluster_traffic_encryption` - See Argument Reference above. From 4d9b396cfc9e5c454d77570f1f10d67cf0e6b8ea Mon Sep 17 00:00:00 2001 From: fabiante Date: Tue, 2 Dec 2025 12:14:33 +0100 Subject: [PATCH 2/7] Add support for pool labels --- gridscale/resource_gridscale_k8s.go | 141 +++++++++++++++++++++++ gridscale/resource_gridscale_k8s_test.go | 107 +++++++++++++++++ website/docs/r/k8s.html.md | 29 +++++ 3 files changed, 277 insertions(+) diff --git a/gridscale/resource_gridscale_k8s.go b/gridscale/resource_gridscale_k8s.go index cf92cccd..3a758bb3 100644 --- a/gridscale/resource_gridscale_k8s.go +++ b/gridscale/resource_gridscale_k8s.go @@ -26,6 +26,7 @@ const ( k8sRocketStorageSupportRelease = "1.26" k8sMultiNodePoolSupportRelease = "1.30" k8sTaintKeyValueRegex = `^[a-zA-Z0-9-]+$` + k8sLabelKeyValueRegex = `^[a-zA-Z0-9-]+$` ) // ResourceGridscaleK8sModeler struct represents a modeler of the gridscale k8s resource. @@ -145,6 +146,27 @@ func (rgk8sm *ResourceGridscaleK8sModeler) buildInputSchema() map[string]*schema }, }, }, + "labels": { + Type: schema.TypeList, + Optional: true, + Description: "List of labels to be applied to the nodes of this pool.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Required: true, + Description: "The key of the label.", + ValidateFunc: validation.StringMatch(regexp.MustCompile(k8sLabelKeyValueRegex), "key must match Kubernetes label key format"), + }, + "value": { + Type: schema.TypeString, + Required: true, + Description: "The value of the label.", + ValidateFunc: validation.StringMatch(regexp.MustCompile(k8sLabelKeyValueRegex), "value must match Kubernetes label value format"), + }, + }, + }, + }, } return map[string]*schema.Schema{ "name": { @@ -906,6 +928,28 @@ func resourceGridscaleK8sRead(d *schema.ResourceData, meta interface{}) error { nodePoolRead["taints"] = taintsRead } + // Handle labels + if labels, isLabelsSet := nodePoolSet["labels"]; isLabelsSet { + labelsList := labels.([]any) + labelsRead := make([]map[string]any, 0) + + for _, labelInterface := range labelsList { + label := labelInterface.(map[string]any) + labelRead := make(map[string]any) + + if key, isKeySet := label["key"]; isKeySet { + labelRead["key"] = key + } + if value, isValueSet := label["value"]; isValueSet { + labelRead["value"] = value + } + + labelsRead = append(labelsRead, labelRead) + } + + nodePoolRead["labels"] = labelsRead + } + nodePools = append(nodePools, nodePoolRead) } } @@ -1031,6 +1075,28 @@ func resourceGridscaleK8sCreate(d *schema.ResourceData, meta interface{}) error nodePool["taints"] = taintsRequest } + // Handle labels + if labelsInterface, isLabelsSet := d.GetOk(fmt.Sprintf("node_pool.%d.labels", index)); isLabelsSet { + labelsList := labelsInterface.([]any) + labelsRequest := make([]map[string]any, 0) + + for _, labelInterface := range labelsList { + label := labelInterface.(map[string]any) + labelRequest := make(map[string]any) + + if key, isKeySet := label["key"]; isKeySet { + labelRequest["key"] = key + } + if value, isValueSet := label["value"]; isValueSet { + labelRequest["value"] = value + } + + labelsRequest = append(labelsRequest, labelRequest) + } + + nodePool["labels"] = labelsRequest + } + nodePools = append(nodePools, nodePool) } parameters["pools"] = nodePools @@ -1217,6 +1283,28 @@ func resourceGridscaleK8sUpdate(d *schema.ResourceData, meta interface{}) error nodePool["taints"] = taintsRequest } + // Handle labels + if labelsInterface, isLabelsSet := d.GetOk(fmt.Sprintf("node_pool.%d.labels", index)); isLabelsSet { + labelsList := labelsInterface.([]any) + labelsRequest := make([]map[string]any, 0) + + for _, labelInterface := range labelsList { + label := labelInterface.(map[string]any) + labelRequest := make(map[string]any) + + if key, isKeySet := label["key"]; isKeySet { + labelRequest["key"] = key + } + if value, isValueSet := label["value"]; isValueSet { + labelRequest["value"] = value + } + + labelsRequest = append(labelsRequest, labelRequest) + } + + nodePool["labels"] = labelsRequest + } + nodePools = append(nodePools, nodePool) } parameters["pools"] = nodePools @@ -1559,6 +1647,59 @@ func validateK8sParameters(d *schema.ResourceDiff, template gsclient.PaaSTemplat } } } + + // Validate labels + nodePoolParameterLabels, labels_ok := templateParameterNodePools.Schema.Schema["labels"] + if labels_ok { + if labelsInterface, isLabelsSet := d.GetOk(fmt.Sprintf("node_pool.%d.labels", index)); isLabelsSet { + labelsList := labelsInterface.([]any) + + // Check if labels list is empty when it's allowed to be + if len(labelsList) == 0 && !nodePoolParameterLabels.Empty { + errorMessages = append( + errorMessages, + fmt.Sprintf("Invalid 'node_pool.%d.labels' value. Labels list cannot be empty.\n", index), + ) + } + + // Validate each label + for _, labelInterface := range labelsList { + label := labelInterface.(map[string]any) + + // Validate key + if key, isKeySet := label["key"]; isKeySet { + keyStr := key.(string) + if !regexp.MustCompile(k8sLabelKeyValueRegex).MatchString(keyStr) { + errorMessages = append( + errorMessages, + fmt.Sprintf("Invalid 'node_pool.%d.labels.key' value. Key must match Kubernetes label key format.\n", index), + ) + } + } else { + errorMessages = append( + errorMessages, + fmt.Sprintf("Invalid 'node_pool.%d.labels' value. Key is required.\n", index), + ) + } + + // Validate value + if value, isValueSet := label["value"]; isValueSet { + valueStr := value.(string) + if !regexp.MustCompile(k8sLabelKeyValueRegex).MatchString(valueStr) { + errorMessages = append( + errorMessages, + fmt.Sprintf("Invalid 'node_pool.%d.labels.value' value. Value must match Kubernetes label value format.\n", index), + ) + } + } else { + errorMessages = append( + errorMessages, + fmt.Sprintf("Invalid 'node_pool.%d.labels' value. Value is required.\n", index), + ) + } + } + } + } } } diff --git a/gridscale/resource_gridscale_k8s_test.go b/gridscale/resource_gridscale_k8s_test.go index 0bd2ded7..3a0c784a 100644 --- a/gridscale/resource_gridscale_k8s_test.go +++ b/gridscale/resource_gridscale_k8s_test.go @@ -165,6 +165,66 @@ func TestAccResourceGridscaleK8sBasic(t *testing.T) { "gridscale_k8s.foopaas", "surge_node", "true"), ), }, + { + Config: testAccCheckResourceGridscaleK8sConfigAddLabel(), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceGridscalePaaSExists("gridscale_k8s.foopaas", &object), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.name", "my-node-pool"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.node_count", "1"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.cores", "2"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.memory", "4"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.storage", "50"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.storage_type", "storage_insane"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.rocket_storage", "10"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.labels.#", "2"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.labels.0.key", "example-key"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.labels.0.value", "example-value"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.labels.1.key", "another-key"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.labels.1.value", "another-value"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "k8s_hubble", "true"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "surge_node", "true"), + ), + }, + { + Config: testAccCheckResourceGridscaleK8sConfigRemoveLabel(), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceGridscalePaaSExists("gridscale_k8s.foopaas", &object), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.name", "my-node-pool"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.node_count", "1"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.cores", "2"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.memory", "4"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.storage", "50"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.storage_type", "storage_insane"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.rocket_storage", "10"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "node_pool.0.labels.#", "0"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "k8s_hubble", "true"), + resource.TestCheckResourceAttr( + "gridscale_k8s.foopaas", "surge_node", "true"), + ), + }, { Config: testAccCheckResourceGridscaleK8sConfigNodeCountIncrease(), Check: resource.ComposeTestCheckFunc( @@ -405,6 +465,53 @@ func testAccCheckResourceGridscaleK8sConfigRemoveTaint() string { ` } +func testAccCheckResourceGridscaleK8sConfigAddLabel() string { + return ` + resource "gridscale_k8s" "foopaas" { + name = "newname" + release = "1.32" + node_pool { + name = "my-node-pool" + node_count = 1 + cores = 2 + memory = 4 + storage = 50 + storage_type = "storage_insane" + rocket_storage = 10 + labels { + key = "example-key" + value = "example-value" + } + labels { + key = "another-key" + value = "another-value" + } + } + k8s_hubble = true + } + ` +} + +func testAccCheckResourceGridscaleK8sConfigRemoveLabel() string { + return ` + resource "gridscale_k8s" "foopaas" { + name = "newname" + release = "1.32" + node_pool { + name = "my-node-pool" + node_count = 1 + cores = 2 + memory = 4 + storage = 50 + storage_type = "storage_insane" + rocket_storage = 10 + labels = [] + } + k8s_hubble = true + } + ` +} + func testAccCheckResourceGridscaleK8sConfigNodeCountDecrease() string { return ` resource "gridscale_k8s" "foopaas" { diff --git a/website/docs/r/k8s.html.md b/website/docs/r/k8s.html.md index 1e77c147..7eaafd75 100644 --- a/website/docs/r/k8s.html.md +++ b/website/docs/r/k8s.html.md @@ -63,6 +63,7 @@ The following arguments are supported: * `storage_type` - Storage type (one of storage, storage_high, storage_insane). * `rocket_storage` - Rocket storage per worker node (in GiB). * `taints` - List of taints to be applied to the nodes of this pool. + * `labels` - List of labels to be applied to the nodes of this pool. * `surge_node` - Enable surge node to avoid resources shortage during the cluster upgrade (Default: true). * `cluster_cidr` - (Immutable) The cluster CIDR that will be used to generate the CIDR of nodes, services, and pods. The allowed CIDR prefix length is /16. If the cluster CIDR is not set, the cluster will use "10.244.0.0/16" as it default (even though the `cluster_cidr` in the k8s resource is empty). * `cluster_traffic_encryption` - Enables cluster encryption via wireguard if true. Only available for GSK version 1.29 and above. Default is false. @@ -120,6 +121,33 @@ resource "gridscale_k8s" "k8s-test" { } ``` +#### Pool Labels + +```terraform +resource "gridscale_k8s" "k8s-test" { + name = "test" + release = "1.30" # instead, gsk_version can be set. + + node_pool { + name = "pool-0" + node_count = 2 + cores = 2 + memory = 4 + storage = 30 + storage_type = "storage_insane" + + labels { + key = "example-key" + value = "example-value" + } + labels { + key = "another-key" + value = "another-value" + } + } +} +``` + ## Timeouts Timeouts configuration options (in seconds): @@ -152,6 +180,7 @@ This resource exports the following attributes: * `storage_type` - See Argument Reference above. * `rocket_storage` - See Argument Reference above. * `taints` - See Argument Reference above. + * `labels` - See Argument Reference above. * `surge_node` - See Argument Reference above. * `cluster_cidr` - See Argument Reference above. * `cluster_traffic_encryption` - See Argument Reference above. From b19464ad2231a1cd784fad964e6bcf76e6ff7834 Mon Sep 17 00:00:00 2001 From: fabiante Date: Tue, 2 Dec 2025 12:14:33 +0100 Subject: [PATCH 3/7] Improve docs --- website/docs/r/k8s.html.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/docs/r/k8s.html.md b/website/docs/r/k8s.html.md index 7eaafd75..3014b9ae 100644 --- a/website/docs/r/k8s.html.md +++ b/website/docs/r/k8s.html.md @@ -62,8 +62,8 @@ The following arguments are supported: * `storage` - Storage per worker node (in GiB). * `storage_type` - Storage type (one of storage, storage_high, storage_insane). * `rocket_storage` - Rocket storage per worker node (in GiB). - * `taints` - List of taints to be applied to the nodes of this pool. - * `labels` - List of labels to be applied to the nodes of this pool. + * `taints` - List of taints to be applied to the nodes of this pool. Check the [product documentation](https://my.gridscale.io/product-documentation/cloud-computing/products/paas/kubernetes/node-pools/introduction/#support-for-taints-and-labels) for details + * `labels` - List of labels to be applied to the nodes of this pool. Check the [product documentation](https://my.gridscale.io/product-documentation/cloud-computing/products/paas/kubernetes/node-pools/introduction/#support-for-taints-and-labels) for details * `surge_node` - Enable surge node to avoid resources shortage during the cluster upgrade (Default: true). * `cluster_cidr` - (Immutable) The cluster CIDR that will be used to generate the CIDR of nodes, services, and pods. The allowed CIDR prefix length is /16. If the cluster CIDR is not set, the cluster will use "10.244.0.0/16" as it default (even though the `cluster_cidr` in the k8s resource is empty). * `cluster_traffic_encryption` - Enables cluster encryption via wireguard if true. Only available for GSK version 1.29 and above. Default is false. @@ -97,7 +97,7 @@ The following arguments are supported: ```terraform resource "gridscale_k8s" "k8s-test" { name = "test" - release = "1.30" # instead, gsk_version can be set. + release = "1.32" # instead, gsk_version can be set. node_pool { name = "pool-0" @@ -126,7 +126,7 @@ resource "gridscale_k8s" "k8s-test" { ```terraform resource "gridscale_k8s" "k8s-test" { name = "test" - release = "1.30" # instead, gsk_version can be set. + release = "1.32" # instead, gsk_version can be set. node_pool { name = "pool-0" From 9b2f4eb32ee1003b65475c3b6fd8749f2108d363 Mon Sep 17 00:00:00 2001 From: fabiante Date: Tue, 2 Dec 2025 12:59:00 +0100 Subject: [PATCH 4/7] Remove regex validation on taints and labels The API already does the validation. I don't want to copy the APIs regex into this project, because it will likely result in drift between the values. --- gridscale/resource_gridscale_k8s.go | 70 +++++++++-------------------- 1 file changed, 20 insertions(+), 50 deletions(-) diff --git a/gridscale/resource_gridscale_k8s.go b/gridscale/resource_gridscale_k8s.go index 3a758bb3..2c42c831 100644 --- a/gridscale/resource_gridscale_k8s.go +++ b/gridscale/resource_gridscale_k8s.go @@ -25,8 +25,6 @@ const ( k8sLabelPrefix = "#gsk#" k8sRocketStorageSupportRelease = "1.26" k8sMultiNodePoolSupportRelease = "1.30" - k8sTaintKeyValueRegex = `^[a-zA-Z0-9-]+$` - k8sLabelKeyValueRegex = `^[a-zA-Z0-9-]+$` ) // ResourceGridscaleK8sModeler struct represents a modeler of the gridscale k8s resource. @@ -126,16 +124,14 @@ func (rgk8sm *ResourceGridscaleK8sModeler) buildInputSchema() map[string]*schema Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "key": { - Type: schema.TypeString, - Required: true, - Description: "The key of the taint.", - ValidateFunc: validation.StringMatch(regexp.MustCompile(k8sTaintKeyValueRegex), "key must consist of alphanumeric characters and hyphens only"), + Type: schema.TypeString, + Required: true, + Description: "The key of the taint.", }, "value": { - Type: schema.TypeString, - Required: true, - Description: "The value of the taint.", - ValidateFunc: validation.StringMatch(regexp.MustCompile(k8sTaintKeyValueRegex), "value must consist of alphanumeric characters and hyphens only"), + Type: schema.TypeString, + Required: true, + Description: "The value of the taint.", }, "effect": { Type: schema.TypeString, @@ -153,16 +149,14 @@ func (rgk8sm *ResourceGridscaleK8sModeler) buildInputSchema() map[string]*schema Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "key": { - Type: schema.TypeString, - Required: true, - Description: "The key of the label.", - ValidateFunc: validation.StringMatch(regexp.MustCompile(k8sLabelKeyValueRegex), "key must match Kubernetes label key format"), + Type: schema.TypeString, + Required: true, + Description: "The key of the label.", }, "value": { - Type: schema.TypeString, - Required: true, - Description: "The value of the label.", - ValidateFunc: validation.StringMatch(regexp.MustCompile(k8sLabelKeyValueRegex), "value must match Kubernetes label value format"), + Type: schema.TypeString, + Required: true, + Description: "The value of the label.", }, }, }, @@ -1590,14 +1584,8 @@ func validateK8sParameters(d *schema.ResourceDiff, template gsclient.PaaSTemplat taint := taintInterface.(map[string]any) // Validate key - if key, isKeySet := taint["key"]; isKeySet { - keyStr := key.(string) - if !regexp.MustCompile(k8sTaintKeyValueRegex).MatchString(keyStr) { - errorMessages = append( - errorMessages, - fmt.Sprintf("Invalid 'node_pool.%d.taints.key' value. Key must consist of alphanumeric characters and hyphens only.\n", index), - ) - } + if _, isKeySet := taint["key"]; isKeySet { + // Key validation removed } else { errorMessages = append( errorMessages, @@ -1606,14 +1594,8 @@ func validateK8sParameters(d *schema.ResourceDiff, template gsclient.PaaSTemplat } // Validate value - if value, isValueSet := taint["value"]; isValueSet { - valueStr := value.(string) - if !regexp.MustCompile(k8sTaintKeyValueRegex).MatchString(valueStr) { - errorMessages = append( - errorMessages, - fmt.Sprintf("Invalid 'node_pool.%d.taints.value' value. Value must consist of alphanumeric characters and hyphens only.\n", index), - ) - } + if _, isValueSet := taint["value"]; isValueSet { + // Value validation removed } else { errorMessages = append( errorMessages, @@ -1667,14 +1649,8 @@ func validateK8sParameters(d *schema.ResourceDiff, template gsclient.PaaSTemplat label := labelInterface.(map[string]any) // Validate key - if key, isKeySet := label["key"]; isKeySet { - keyStr := key.(string) - if !regexp.MustCompile(k8sLabelKeyValueRegex).MatchString(keyStr) { - errorMessages = append( - errorMessages, - fmt.Sprintf("Invalid 'node_pool.%d.labels.key' value. Key must match Kubernetes label key format.\n", index), - ) - } + if _, isKeySet := label["key"]; isKeySet { + // Key validation removed } else { errorMessages = append( errorMessages, @@ -1683,14 +1659,8 @@ func validateK8sParameters(d *schema.ResourceDiff, template gsclient.PaaSTemplat } // Validate value - if value, isValueSet := label["value"]; isValueSet { - valueStr := value.(string) - if !regexp.MustCompile(k8sLabelKeyValueRegex).MatchString(valueStr) { - errorMessages = append( - errorMessages, - fmt.Sprintf("Invalid 'node_pool.%d.labels.value' value. Value must match Kubernetes label value format.\n", index), - ) - } + if _, isValueSet := label["value"]; isValueSet { + // Value validation removed } else { errorMessages = append( errorMessages, From d21ccf9410d6672a241d392a6165bdf5d618add9 Mon Sep 17 00:00:00 2001 From: fabiante Date: Tue, 2 Dec 2025 13:22:28 +0100 Subject: [PATCH 5/7] Change all k8s versions to 1.32 in tests --- gridscale/resource_gridscale_k8s_test.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/gridscale/resource_gridscale_k8s_test.go b/gridscale/resource_gridscale_k8s_test.go index 3a0c784a..5f6fc144 100644 --- a/gridscale/resource_gridscale_k8s_test.go +++ b/gridscale/resource_gridscale_k8s_test.go @@ -28,7 +28,7 @@ func TestAccResourceGridscaleK8sBasic(t *testing.T) { resource.TestCheckResourceAttr( "gridscale_k8s.foopaas", "name", name), resource.TestCheckResourceAttr( - "gridscale_k8s.foopaas", "release", "1.30"), + "gridscale_k8s.foopaas", "release", "1.32"), resource.TestCheckResourceAttr( "gridscale_k8s.foopaas", "node_pool.0.name", "my-node-pool"), resource.TestCheckResourceAttr( @@ -56,7 +56,7 @@ func TestAccResourceGridscaleK8sBasic(t *testing.T) { resource.TestCheckResourceAttr( "gridscale_k8s.foopaas", "name", "newname"), resource.TestCheckResourceAttr( - "gridscale_k8s.foopaas", "release", "1.30"), + "gridscale_k8s.foopaas", "release", "1.32"), resource.TestCheckResourceAttr( "gridscale_k8s.foopaas", "node_pool.0.name", "my-node-pool"), resource.TestCheckResourceAttr( @@ -344,7 +344,7 @@ func testAccCheckResourceGridscaleK8sConfigBasic(name string) string { return fmt.Sprintf(` resource "gridscale_k8s" "foopaas" { name = "%s" - release = "1.30" + release = "1.32" node_pool { name = "my-node-pool" node_count = 1 @@ -363,7 +363,7 @@ func testAccCheckResourceGridscaleK8sConfigBasicUpdate() string { return ` resource "gridscale_k8s" "foopaas" { name = "newname" - release = "1.30" + release = "1.32" node_pool { name = "my-node-pool" node_count = 1 @@ -382,7 +382,7 @@ func testAccCheckResourceGridscaleK8sConfigNodePoolSpecsUpdate() string { return ` resource "gridscale_k8s" "foopaas" { name = "newname" - release = "1.30" + release = "1.32" node_pool { name = "my-node-pool" node_count = 1 @@ -401,7 +401,7 @@ func testAccCheckResourceGridscaleK8sConfigNodeCountIncrease() string { return ` resource "gridscale_k8s" "foopaas" { name = "newname" - release = "1.30" + release = "1.32" node_pool { name = "my-node-pool" node_count = 2 @@ -516,7 +516,7 @@ func testAccCheckResourceGridscaleK8sConfigNodeCountDecrease() string { return ` resource "gridscale_k8s" "foopaas" { name = "newname" - release = "1.30" + release = "1.32" node_pool { name = "my-node-pool" node_count = 1 @@ -535,7 +535,7 @@ func testAccCheckResourceGridscaleK8sConfigAddNodePool() string { return ` resource "gridscale_k8s" "foopaas" { name = "newname" - release = "1.30" + release = "1.32" node_pool { name = "my-node-pool" node_count = 1 @@ -562,7 +562,7 @@ func testAccCheckResourceGridscaleK8sConfigRemoveNodePool() string { return ` resource "gridscale_k8s" "foopaas" { name = "newname" - release = "1.30" + release = "1.32" node_pool { name = "my-node-pool" node_count = 1 From 75494fbfbf9341c2efe643f4b6df6d280704a269 Mon Sep 17 00:00:00 2001 From: fabiante Date: Tue, 2 Dec 2025 14:14:03 +0100 Subject: [PATCH 6/7] Add comments to test steps --- gridscale/resource_gridscale_k8s_test.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/gridscale/resource_gridscale_k8s_test.go b/gridscale/resource_gridscale_k8s_test.go index 5f6fc144..96fb9800 100644 --- a/gridscale/resource_gridscale_k8s_test.go +++ b/gridscale/resource_gridscale_k8s_test.go @@ -22,6 +22,7 @@ func TestAccResourceGridscaleK8sBasic(t *testing.T) { CheckDestroy: testAccCheckResourceGridscalePaaSDestroyCheck, Steps: []resource.TestStep{ { + // Provision the cluster Config: testAccCheckResourceGridscaleK8sConfigBasic(name), Check: resource.ComposeTestCheckFunc( testAccCheckResourceGridscalePaaSExists("gridscale_k8s.foopaas", &object), @@ -50,6 +51,7 @@ func TestAccResourceGridscaleK8sBasic(t *testing.T) { ), }, { + // Update the cluster name Config: testAccCheckResourceGridscaleK8sConfigBasicUpdate(), Check: resource.ComposeTestCheckFunc( testAccCheckResourceGridscalePaaSExists("gridscale_k8s.foopaas", &object), @@ -78,6 +80,7 @@ func TestAccResourceGridscaleK8sBasic(t *testing.T) { ), }, { + // Change node pools vm spec - triggers a recycle Config: testAccCheckResourceGridscaleK8sConfigNodePoolSpecsUpdate(), Check: resource.ComposeTestCheckFunc( testAccCheckResourceGridscalePaaSExists("gridscale_k8s.foopaas", &object), @@ -102,6 +105,7 @@ func TestAccResourceGridscaleK8sBasic(t *testing.T) { ), }, { + // Add a taint to the node pool Config: testAccCheckResourceGridscaleK8sConfigAddTaint(), Check: resource.ComposeTestCheckFunc( testAccCheckResourceGridscalePaaSExists("gridscale_k8s.foopaas", &object), @@ -140,6 +144,7 @@ func TestAccResourceGridscaleK8sBasic(t *testing.T) { ), }, { + // Remove the taint Config: testAccCheckResourceGridscaleK8sConfigRemoveTaint(), Check: resource.ComposeTestCheckFunc( testAccCheckResourceGridscalePaaSExists("gridscale_k8s.foopaas", &object), @@ -166,6 +171,7 @@ func TestAccResourceGridscaleK8sBasic(t *testing.T) { ), }, { + // Add a label to the node pool Config: testAccCheckResourceGridscaleK8sConfigAddLabel(), Check: resource.ComposeTestCheckFunc( testAccCheckResourceGridscalePaaSExists("gridscale_k8s.foopaas", &object), @@ -200,6 +206,7 @@ func TestAccResourceGridscaleK8sBasic(t *testing.T) { ), }, { + // Remove the label Config: testAccCheckResourceGridscaleK8sConfigRemoveLabel(), Check: resource.ComposeTestCheckFunc( testAccCheckResourceGridscalePaaSExists("gridscale_k8s.foopaas", &object), @@ -226,6 +233,7 @@ func TestAccResourceGridscaleK8sBasic(t *testing.T) { ), }, { + // Add another node to the pool Config: testAccCheckResourceGridscaleK8sConfigNodeCountIncrease(), Check: resource.ComposeTestCheckFunc( testAccCheckResourceGridscalePaaSExists("gridscale_k8s.foopaas", &object), @@ -250,6 +258,7 @@ func TestAccResourceGridscaleK8sBasic(t *testing.T) { ), }, { + // Remove a node from the pool Config: testAccCheckResourceGridscaleK8sConfigNodeCountDecrease(), Check: resource.ComposeTestCheckFunc( testAccCheckResourceGridscalePaaSExists("gridscale_k8s.foopaas", &object), @@ -274,6 +283,7 @@ func TestAccResourceGridscaleK8sBasic(t *testing.T) { ), }, { + // Add a node pool Config: testAccCheckResourceGridscaleK8sConfigAddNodePool(), Check: resource.ComposeTestCheckFunc( testAccCheckResourceGridscalePaaSExists("gridscale_k8s.foopaas", &object), @@ -310,6 +320,7 @@ func TestAccResourceGridscaleK8sBasic(t *testing.T) { ), }, { + // Remove the node pool Config: testAccCheckResourceGridscaleK8sConfigRemoveNodePool(), Check: resource.ComposeTestCheckFunc( testAccCheckResourceGridscalePaaSExists("gridscale_k8s.foopaas", &object), From 49d4d50757688864650694208391a4d74519e015 Mon Sep 17 00:00:00 2001 From: fabiante Date: Tue, 2 Dec 2025 14:22:56 +0100 Subject: [PATCH 7/7] Fix test failure due to empty taints definition --- gridscale/resource_gridscale_k8s_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/gridscale/resource_gridscale_k8s_test.go b/gridscale/resource_gridscale_k8s_test.go index 96fb9800..656aed45 100644 --- a/gridscale/resource_gridscale_k8s_test.go +++ b/gridscale/resource_gridscale_k8s_test.go @@ -469,7 +469,6 @@ func testAccCheckResourceGridscaleK8sConfigRemoveTaint() string { storage = 50 storage_type = "storage_insane" rocket_storage = 10 - taints = [] } k8s_hubble = true } @@ -516,7 +515,6 @@ func testAccCheckResourceGridscaleK8sConfigRemoveLabel() string { storage = 50 storage_type = "storage_insane" rocket_storage = 10 - labels = [] } k8s_hubble = true }