diff --git a/.changelog/6412.txt b/.changelog/6412.txt new file mode 100644 index 00000000000..5096693852b --- /dev/null +++ b/.changelog/6412.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +container: Added general field `reservation_affinity` to `google_container_node_pool` +``` diff --git a/google/node_config.go b/google/node_config.go index bd6cbf2b95d..680db1a16db 100644 --- a/google/node_config.go +++ b/google/node_config.go @@ -188,7 +188,39 @@ func schemaNodeConfig() *schema.Schema { Default: false, Description: `Whether the nodes are created as preemptible VM instances.`, }, - + "reservation_affinity": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Description: `The reservation affinity configuration for the node pool.`, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "consume_reservation_type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `Corresponds to the type of reservation consumption.`, + ValidateFunc: validation.StringInSlice([]string{"UNSPECIFIED", "NO_RESERVATION", "ANY_RESERVATION", "SPECIFIC_RESERVATION"}, false), + }, + "key": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: `The label key of a reservation resource.`, + }, + "values": { + Type: schema.TypeSet, + Description: "The label values of the reservation resource.", + ForceNew: true, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + }, "spot": { Type: schema.TypeBool, Optional: true, @@ -369,6 +401,21 @@ func expandNodeConfig(v interface{}) *container.NodeConfig { } } + if v, ok := nodeConfig["reservation_affinity"]; ok && len(v.([]interface{})) > 0 { + conf := v.([]interface{})[0].(map[string]interface{}) + valuesSet := conf["values"].(*schema.Set) + values := make([]string, valuesSet.Len()) + for i, value := range valuesSet.List() { + values[i] = value.(string) + } + + nc.ReservationAffinity = &container.ReservationAffinity{ + ConsumeReservationType: conf["consume_reservation_type"].(string), + Key: conf["key"].(string), + Values: values, + } + } + if scopes, ok := nodeConfig["oauth_scopes"]; ok { scopesSet := scopes.(*schema.Set) scopes := make([]string, scopesSet.Len()) @@ -496,6 +543,7 @@ func flattenNodeConfig(c *container.NodeConfig) []map[string]interface{} { "local_ssd_count": c.LocalSsdCount, "gcfs_config": flattenGcfsConfig(c.GcfsConfig), "gvnic": flattenGvnic(c.Gvnic), + "reservation_affinity": flattenGKEReservationAffinity(c.ReservationAffinity), "service_account": c.ServiceAccount, "metadata": c.Metadata, "image_type": c.ImageType, @@ -561,6 +609,18 @@ func flattenGvnic(c *container.VirtualNIC) []map[string]interface{} { return result } +func flattenGKEReservationAffinity(c *container.ReservationAffinity) []map[string]interface{} { + result := []map[string]interface{}{} + if c != nil { + result = append(result, map[string]interface{}{ + "consume_reservation_type": c.ConsumeReservationType, + "key": c.Key, + "values": c.Values, + }) + } + return result +} + func flattenTaints(c []*container.NodeTaint) []map[string]interface{} { result := []map[string]interface{}{} for _, taint := range c { diff --git a/google/resource_container_cluster_test.go b/google/resource_container_cluster_test.go index a1a43fc182f..8a087270b3e 100755 --- a/google/resource_container_cluster_test.go +++ b/google/resource_container_cluster_test.go @@ -888,6 +888,69 @@ func TestAccContainerCluster_withNodeConfigShieldedInstanceConfig(t *testing.T) }) } +func TestAccContainerCluster_withNodeConfigReservationAffinity(t *testing.T) { + t.Parallel() + + clusterName := fmt.Sprintf("tf-test-cluster-%s", randString(t, 10)) + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckContainerClusterDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccContainerCluster_withNodeConfigReservationAffinity(clusterName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_container_cluster.with_node_config", + "node_config.0.reservation_affinity.#", "1"), + resource.TestCheckResourceAttr("google_container_cluster.with_node_config", + "node_config.0.reservation_affinity.0.consume_reservation_type", "ANY_RESERVATION"), + ), + }, + { + ResourceName: "google_container_cluster.with_node_config", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccContainerCluster_withNodeConfigReservationAffinitySpecific(t *testing.T) { + t.Parallel() + + reservationName := fmt.Sprintf("tf-test-reservation-%s", randString(t, 10)) + clusterName := fmt.Sprintf("tf-test-cluster-%s", randString(t, 10)) + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckContainerClusterDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccContainerCluster_withNodeConfigReservationAffinitySpecific(reservationName, clusterName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_container_cluster.with_node_config", + "node_config.0.reservation_affinity.#", "1"), + resource.TestCheckResourceAttr("google_container_cluster.with_node_config", + "node_config.0.reservation_affinity.0.consume_reservation_type", "SPECIFIC_RESERVATION"), + resource.TestCheckResourceAttr("google_container_cluster.with_node_config", + "node_config.0.reservation_affinity.0.key", "compute.googleapis.com/reservation-name"), + resource.TestCheckResourceAttr("google_container_cluster.with_node_config", + "node_config.0.reservation_affinity.0.values.#", "1"), + resource.TestCheckResourceAttr("google_container_cluster.with_node_config", + "node_config.0.reservation_affinity.0.values.0", reservationName), + ), + }, + { + ResourceName: "google_container_cluster.with_node_config", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func TestAccContainerCluster_withWorkloadMetadataConfig(t *testing.T) { t.Parallel() @@ -3112,6 +3175,116 @@ resource "google_container_cluster" "with_node_config" { `, clusterName) } +func testAccContainerCluster_withNodeConfigReservationAffinity(clusterName string) string { + return fmt.Sprintf(` +resource "google_container_cluster" "with_node_config" { + name = "%s" + location = "us-central1-f" + initial_node_count = 1 + + node_config { + machine_type = "e2-medium" + disk_size_gb = 15 + disk_type = "pd-ssd" + oauth_scopes = [ + "https://www.googleapis.com/auth/monitoring", + "https://www.googleapis.com/auth/compute", + "https://www.googleapis.com/auth/devstorage.read_only", + "https://www.googleapis.com/auth/logging.write", + ] + service_account = "default" + metadata = { + foo = "bar" + disable-legacy-endpoints = "true" + } + labels = { + foo = "bar" + } + tags = ["foo", "bar"] + preemptible = true + + // Updatable fields + image_type = "COS_CONTAINERD" + + reservation_affinity { + consume_reservation_type = "ANY_RESERVATION" + } + } +} +`, clusterName) +} + +func testAccContainerCluster_withNodeConfigReservationAffinitySpecific(reservation, clusterName string) string { + return fmt.Sprintf(` + +resource "google_project_service" "compute" { + service = "compute.googleapis.com" + disable_on_destroy = false +} + +resource "google_project_service" "container" { + service = "container.googleapis.com" + disable_on_destroy = false + depends_on = [google_project_service.compute] +} + + +resource "google_compute_reservation" "gce_reservation" { + name = "%s" + zone = "us-central1-f" + + specific_reservation { + count = 1 + instance_properties { + machine_type = "n1-standard-1" + } + } + + specific_reservation_required = true + depends_on = [google_project_service.compute] +} + +resource "google_container_cluster" "with_node_config" { + name = "%s" + location = "us-central1-f" + initial_node_count = 1 + + node_config { + machine_type = "n1-standard-1" + disk_size_gb = 15 + disk_type = "pd-ssd" + oauth_scopes = [ + "https://www.googleapis.com/auth/monitoring", + "https://www.googleapis.com/auth/compute", + "https://www.googleapis.com/auth/devstorage.read_only", + "https://www.googleapis.com/auth/logging.write", + ] + service_account = "default" + metadata = { + foo = "bar" + disable-legacy-endpoints = "true" + } + labels = { + foo = "bar" + } + tags = ["foo", "bar"] + + // Updatable fields + image_type = "COS_CONTAINERD" + + reservation_affinity { + consume_reservation_type = "SPECIFIC_RESERVATION" + key = "compute.googleapis.com/reservation-name" + values = [ + google_compute_reservation.gce_reservation.name + ] + } + } + depends_on = [google_project_service.container] +} +`, reservation, clusterName) +} + func testAccContainerCluster_withWorkloadMetadataConfig(clusterName string) string { return fmt.Sprintf(` data "google_container_engine_versions" "central1a" { diff --git a/google/resource_container_node_pool_test.go b/google/resource_container_node_pool_test.go index 9fd5870691e..c78fef270d2 100644 --- a/google/resource_container_node_pool_test.go +++ b/google/resource_container_node_pool_test.go @@ -189,6 +189,71 @@ func TestAccContainerNodePool_withNodeConfig(t *testing.T) { }) } +func TestAccContainerNodePool_withReservationAffinity(t *testing.T) { + t.Parallel() + + cluster := fmt.Sprintf("tf-test-cluster-%s", randString(t, 10)) + np := fmt.Sprintf("tf-test-np-%s", randString(t, 10)) + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckContainerClusterDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccContainerNodePool_withReservationAffinity(cluster, np), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_container_node_pool.with_reservation_affinity", + "node_config.0.reservation_affinity.#", "1"), + resource.TestCheckResourceAttr("google_container_node_pool.with_reservation_affinity", + "node_config.0.reservation_affinity.0.consume_reservation_type", "ANY_RESERVATION"), + ), + }, + { + ResourceName: "google_container_node_pool.with_reservation_affinity", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccContainerNodePool_withReservationAffinitySpecific(t *testing.T) { + t.Parallel() + + cluster := fmt.Sprintf("tf-test-cluster-%s", randString(t, 10)) + reservation := fmt.Sprintf("tf-test-reservation-%s", randString(t, 10)) + np := fmt.Sprintf("tf-test-np-%s", randString(t, 10)) + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckContainerClusterDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccContainerNodePool_withReservationAffinitySpecific(cluster, reservation, np), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_container_node_pool.with_reservation_affinity", + "node_config.0.reservation_affinity.#", "1"), + resource.TestCheckResourceAttr("google_container_node_pool.with_reservation_affinity", + "node_config.0.reservation_affinity.0.consume_reservation_type", "SPECIFIC_RESERVATION"), + resource.TestCheckResourceAttr("google_container_node_pool.with_reservation_affinity", + "node_config.0.reservation_affinity.0.key", "compute.googleapis.com/reservation-name"), + resource.TestCheckResourceAttr("google_container_node_pool.with_reservation_affinity", + "node_config.0.reservation_affinity.0.values.#", "1"), + resource.TestCheckResourceAttr("google_container_node_pool.with_reservation_affinity", + "node_config.0.reservation_affinity.0.values.0", reservation), + ), + }, + { + ResourceName: "google_container_node_pool.with_reservation_affinity", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func TestAccContainerNodePool_withWorkloadIdentityConfig(t *testing.T) { t.Parallel() @@ -1224,6 +1289,88 @@ resource "google_container_node_pool" "np_with_node_config" { `, cluster, nodePool) } +func testAccContainerNodePool_withReservationAffinity(cluster, np string) string { + return fmt.Sprintf(` +data "google_container_engine_versions" "central1a" { + location = "us-central1-a" +} + +resource "google_container_cluster" "cluster" { + name = "%s" + location = "us-central1-a" + initial_node_count = 1 + min_master_version = data.google_container_engine_versions.central1a.latest_master_version +} + +resource "google_container_node_pool" "with_reservation_affinity" { + name = "%s" + location = "us-central1-a" + cluster = google_container_cluster.cluster.name + initial_node_count = 1 + node_config { + machine_type = "n1-standard-1" + oauth_scopes = [ + "https://www.googleapis.com/auth/logging.write", + "https://www.googleapis.com/auth/monitoring", + ] + reservation_affinity { + consume_reservation_type = "ANY_RESERVATION" + } + } +} +`, cluster, np) +} + +func testAccContainerNodePool_withReservationAffinitySpecific(cluster, reservation, np string) string { + return fmt.Sprintf(` +data "google_container_engine_versions" "central1a" { + location = "us-central1-a" +} + +resource "google_container_cluster" "cluster" { + name = "%s" + location = "us-central1-a" + initial_node_count = 1 + min_master_version = data.google_container_engine_versions.central1a.latest_master_version +} + +resource "google_compute_reservation" "gce_reservation" { + name = "%s" + zone = "us-central1-a" + + specific_reservation { + count = 1 + instance_properties { + machine_type = "n1-standard-1" + } + } + + specific_reservation_required = true +} + +resource "google_container_node_pool" "with_reservation_affinity" { + name = "%s" + location = "us-central1-a" + cluster = google_container_cluster.cluster.name + initial_node_count = 1 + node_config { + machine_type = "n1-standard-1" + oauth_scopes = [ + "https://www.googleapis.com/auth/logging.write", + "https://www.googleapis.com/auth/monitoring", + ] + reservation_affinity { + consume_reservation_type = "SPECIFIC_RESERVATION" + key = "compute.googleapis.com/reservation-name" + values = [ + google_compute_reservation.gce_reservation.name + ] + } + } +} +`, cluster, reservation, np) +} + func testAccContainerNodePool_withWorkloadMetadataConfig(cluster, np string) string { return fmt.Sprintf(` data "google_container_engine_versions" "central1a" { diff --git a/website/docs/r/container_cluster.html.markdown b/website/docs/r/container_cluster.html.markdown index 7ec84544a48..203854b02e7 100755 --- a/website/docs/r/container_cluster.html.markdown +++ b/website/docs/r/container_cluster.html.markdown @@ -747,6 +747,8 @@ gvnic { are preemptible. See the [official documentation](https://cloud.google.com/container-engine/docs/preemptible-vm) for more information. Defaults to false. +* `reservation_affinity` (Optional) The configuration of the desired reservation which instances could take capacity from. Structure is [documented below](#nested_reservation_affinity). + * `spot` - (Optional) A boolean that represents whether the underlying node VMs are spot. See the [official documentation](https://cloud.google.com/kubernetes-engine/docs/concepts/spot-vms) for more information. Defaults to false. @@ -927,6 +929,19 @@ recommended that you omit the block entirely if the field is not set to `true`. * `enabled` (Optional) - Whether the cluster master is accessible globally or not. +The `reservation_affinity` block supports: + +* `consume_reservation_type` (Required) The type of reservation consumption + Accepted values are: + + * `"UNSPECIFIED"`: Default value. This should not be used. + * `"NO_RESERVATION"`: Do not consume from any reserved capacity. + * `"ANY_RESERVATION"`: Consume any reservation available. + * `"SPECIFIC_RESERVATION"`: Must consume from a specific reservation. Must specify key value fields for specifying the reservations. +* `key` (Optional) The label key of a reservation resource. To target a SPECIFIC_RESERVATION by name, specify "compute.googleapis.com/reservation-name" as the key and specify the name of your reservation as its value. +* `values` (Optional) The list of label values of reservation resources. For example: the name of the specific reservation when using a key of "compute.googleapis.com/reservation-name" + + The `sandbox_config` block supports: * `sandbox_type` (Required) Which sandbox to use for pods in the node pool.