From 3c9371ae5cce313d30733ddcdac7d52c1b4fe63b Mon Sep 17 00:00:00 2001 From: Lachezar Tsonov Date: Fri, 28 Jun 2024 11:20:40 +0300 Subject: [PATCH] KUBE-328: Support setting image family on node configuration (#344) * Add ImageFamily field to terraform * Move image family to eks config * Generate docs * Add test * Fix test reference and add update test --- castai/resource_node_configuration.go | 56 ++++++++++++++++++- .../resource_node_configuration_eks_test.go | 4 ++ docs/resources/node_configuration.md | 3 +- 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/castai/resource_node_configuration.go b/castai/resource_node_configuration.go index cb4e260f..7fa579de 100644 --- a/castai/resource_node_configuration.go +++ b/castai/resource_node_configuration.go @@ -39,6 +39,13 @@ const ( FieldNodeConfigurationKOPS = "kops" FieldNodeConfigurationGKE = "gke" FieldNodeConfigurationEKSTargetGroup = "target_group" + FieldNodeConfigurationEKSImageFamily = "eks_image_family" +) + +const ( + eksImageFamilyAL2 = "al2" + eksImageFamilyAL2023 = "al2023" + eksImageFamilyBottlerocket = "bottlerocket" ) func resourceNodeConfiguration() *schema.Resource { @@ -111,7 +118,7 @@ func resourceNodeConfiguration() *schema.Resource { FieldNodeConfigurationImage: { Type: schema.TypeString, Optional: true, - Description: "Image to be used while provisioning the node. If nothing is provided will be resolved to latest available image based on Kubernetes version if possible ", + Description: "Image to be used while provisioning the node. If nothing is provided will be resolved to latest available image based on Image family, Kubernetes version and node architecture if possible. See Cast.ai documentation for details.", ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotWhiteSpace), }, FieldNodeConfigurationTags: { @@ -233,6 +240,15 @@ func resourceNodeConfiguration() *schema.Resource { Description: "Number of IPs per prefix to be used for calculating max pods.", ValidateDiagFunc: validation.ToDiagFunc(validation.IntBetween(0, 256)), }, + FieldNodeConfigurationEKSImageFamily: { + Type: schema.TypeString, + Optional: true, + Description: "Image OS Family to use when provisioning node. If both image and family are provided, the system will use provided image and provisioning logic for given family. If only image family is provided, the system will attempt to resolve the latest image from that family based on kubernetes version and node architecture. If image family is omitted, a default family (based on cloud provider) will be used. See Cast.ai documentation for details.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{eksImageFamilyAL2, eksImageFamilyAL2023, eksImageFamilyBottlerocket}, true)), + DiffSuppressFunc: func(k, oldValue, newValue string, d *schema.ResourceData) bool { + return strings.EqualFold(oldValue, newValue) + }, + }, FieldNodeConfigurationEKSTargetGroup: { Type: schema.TypeList, Optional: true, @@ -678,9 +694,30 @@ func toEKSConfig(obj map[string]interface{}) *sdk.NodeconfigV1EKSConfig { } } + if v, ok := obj[FieldNodeConfigurationEKSImageFamily].(string); ok { + out.ImageFamily = toEKSImageFamily(v) + } + return out } +func toEKSImageFamily(v string) *sdk.NodeconfigV1EKSConfigImageFamily { + if v == "" { + return nil + } + + switch strings.ToLower(v) { + case eksImageFamilyAL2: + return lo.ToPtr(sdk.FAMILYAL2) + case eksImageFamilyAL2023: + return lo.ToPtr(sdk.FAMILYAL2023) + case eksImageFamilyBottlerocket: + return lo.ToPtr(sdk.FAMILYBOTTLEROCKET) + default: + return nil + } +} + func flattenEKSConfig(config *sdk.NodeconfigV1EKSConfig) []map[string]interface{} { if config == nil { return nil @@ -745,9 +782,26 @@ func flattenEKSConfig(config *sdk.NodeconfigV1EKSConfig) []map[string]interface{ } } + if v := config.ImageFamily; v != nil { + m[FieldNodeConfigurationEKSImageFamily] = fromEKSImageFamily(*v) + } + return []map[string]interface{}{m} } +func fromEKSImageFamily(family sdk.NodeconfigV1EKSConfigImageFamily) string { + switch family { + case sdk.FAMILYBOTTLEROCKET, sdk.FamilyBottlerocket: + return eksImageFamilyBottlerocket + case sdk.FAMILYAL2, sdk.FamilyAl2: + return eksImageFamilyAL2 + case sdk.FAMILYAL2023, sdk.FamilyAl2023: + return eksImageFamilyAL2023 + default: + return "" + } +} + func toKOPSConfig(obj map[string]interface{}) *sdk.NodeconfigV1KOPSConfig { if obj == nil { return nil diff --git a/castai/resource_node_configuration_eks_test.go b/castai/resource_node_configuration_eks_test.go index 73b5e45c..3821d144 100644 --- a/castai/resource_node_configuration_eks_test.go +++ b/castai/resource_node_configuration_eks_test.go @@ -50,6 +50,7 @@ func TestAccResourceNodeConfiguration_eks(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "eks.0.target_group.#", "1"), resource.TestCheckResourceAttr(resourceName, "eks.0.max_pods_per_node_formula", "NUM_IP_PER_PREFIX-NUM_MAX_NET_INTERFACES"), resource.TestCheckResourceAttr(resourceName, "eks.0.ips_per_prefix", "4"), + resource.TestCheckResourceAttr(resourceName, "eks.0.eks_image_family", "al2023"), resource.TestCheckResourceAttr(resourceName, "eks.0.target_group.0.arn", "arn:aws:test"), resource.TestCheckResourceAttr(resourceName, "aks.#", "0"), resource.TestCheckResourceAttr(resourceName, "kops.#", "0"), @@ -82,6 +83,7 @@ func TestAccResourceNodeConfiguration_eks(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "eks.0.volume_throughput", "130"), resource.TestCheckResourceAttr(resourceName, "eks.0.max_pods_per_node_formula", "NUM_IP_PER_PREFIX+NUM_MAX_NET_INTERFACES"), resource.TestCheckResourceAttr(resourceName, "eks.0.ips_per_prefix", "3"), + resource.TestCheckResourceAttr(resourceName, "eks.0.eks_image_family", "al2"), resource.TestCheckResourceAttr(resourceName, "eks.0.target_group.#", "1"), resource.TestCheckResourceAttr(resourceName, "eks.0.target_group.0.arn", "arn:aws:test2"), resource.TestCheckResourceAttr(resourceName, "eks.0.target_group.0.port", "80"), @@ -138,6 +140,7 @@ resource "castai_node_configuration" "test" { imds_hop_limit = 3 max_pods_per_node_formula = "NUM_IP_PER_PREFIX-NUM_MAX_NET_INTERFACES" ips_per_prefix = 4 + eks_image_family = "al2023" target_group { arn = "arn:aws:test" } @@ -169,6 +172,7 @@ resource "castai_node_configuration" "test" { volume_throughput = 130 max_pods_per_node_formula = "NUM_IP_PER_PREFIX+NUM_MAX_NET_INTERFACES" ips_per_prefix = 3 + eks_image_family = "al2" target_group { arn = "arn:aws:test2" port = 80 diff --git a/docs/resources/node_configuration.md b/docs/resources/node_configuration.md index ae8a1da2..d6e2a57c 100644 --- a/docs/resources/node_configuration.md +++ b/docs/resources/node_configuration.md @@ -69,7 +69,7 @@ resource "castai_node_configuration" "default" { - `drain_timeout_sec` (Number) Timeout in seconds for draining the node. Defaults to 0 - `eks` (Block List, Max: 1) (see [below for nested schema](#nestedblock--eks)) - `gke` (Block List, Max: 1) (see [below for nested schema](#nestedblock--gke)) -- `image` (String) Image to be used while provisioning the node. If nothing is provided will be resolved to latest available image based on Kubernetes version if possible +- `image` (String) Image to be used while provisioning the node. If nothing is provided will be resolved to latest available image based on Image family, Kubernetes version and node architecture if possible. See Cast.ai documentation for details. - `init_script` (String) Init script to be run on your instance at launch. Should not contain any sensitive data. Value should be base64 encoded - `kops` (Block List, Max: 1) (see [below for nested schema](#nestedblock--kops)) - `kubelet_config` (String) Optional kubelet configuration properties in JSON format. Provide only properties that you want to override. Applicable for EKS only. [Available values](https://kubernetes.io/docs/reference/config-api/kubelet-config.v1beta1/) @@ -102,6 +102,7 @@ Required: Optional: - `dns_cluster_ip` (String) IP address to use for DNS queries within the cluster +- `eks_image_family` (String) Image OS Family to use when provisioning node. If both image and family are provided, the system will use provided image and provisioning logic for given family. If only image family is provided, the system will attempt to resolve the latest image from that family based on kubernetes version and node architecture. If image family is omitted, a default family (based on cloud provider) will be used. See Cast.ai documentation for details. - `imds_hop_limit` (Number) Allow configure the IMDSv2 hop limit, the default is 2 - `imds_v1` (Boolean) When the value is true both IMDSv1 and IMDSv2 are enabled. Setting the value to false disables permanently IMDSv1 and might affect legacy workloads running on the node created with this configuration. The default is true if the flag isn't provided - `ips_per_prefix` (Number) Number of IPs per prefix to be used for calculating max pods.