Skip to content

Commit 43c73eb

Browse files
Breaking change: Rework taint model in GKE (#9011) (#6351)
Signed-off-by: Modular Magician <[email protected]>
1 parent 9fa50f5 commit 43c73eb

7 files changed

+99
-183
lines changed

.changelog/9011.txt

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
```release-note:breaking-change
2+
container: reworked the `taint` field in `google_container_cluster` and `google_container_node_pool` to only manage a subset of taint keys based on those already in state. Most existing resources are unaffected, unless they use `sandbox_config`- see upgrade guide for details.
3+
```
4+
```release-note:enhancement
5+
container: added the `effective_taints` attribute to `google_container_cluster` and `google_container_node_pool`, outputting all known taint values
6+
```

google-beta/services/container/node_config.go

+71-81
Original file line numberDiff line numberDiff line change
@@ -402,16 +402,10 @@ func schemaNodeConfig() *schema.Schema {
402402
},
403403

404404
"taint": {
405-
Type: schema.TypeList,
406-
Optional: true,
407-
// Computed=true because GKE Sandbox will automatically add taints to nodes that can/cannot run sandboxed pods.
408-
Computed: true,
409-
ForceNew: true,
410-
// Legacy config mode allows explicitly defining an empty taint.
411-
// See https://www.terraform.io/docs/configuration/attr-as-blocks.html
412-
ConfigMode: schema.SchemaConfigModeAttr,
413-
Description: `List of Kubernetes taints to be applied to each node.`,
414-
DiffSuppressFunc: containerNodePoolTaintSuppress,
405+
Type: schema.TypeList,
406+
Optional: true,
407+
ForceNew: true,
408+
Description: `List of Kubernetes taints to be applied to each node.`,
415409
Elem: &schema.Resource{
416410
Schema: map[string]*schema.Schema{
417411
"key": {
@@ -437,6 +431,31 @@ func schemaNodeConfig() *schema.Schema {
437431
},
438432
},
439433

434+
"effective_taints": {
435+
Type: schema.TypeList,
436+
Computed: true,
437+
Description: `List of kubernetes taints applied to each node.`,
438+
Elem: &schema.Resource{
439+
Schema: map[string]*schema.Schema{
440+
"key": {
441+
Type: schema.TypeString,
442+
Computed: true,
443+
Description: `Key for taint.`,
444+
},
445+
"value": {
446+
Type: schema.TypeString,
447+
Computed: true,
448+
Description: `Value for taint.`,
449+
},
450+
"effect": {
451+
Type: schema.TypeString,
452+
Computed: true,
453+
Description: `Effect for taint.`,
454+
},
455+
},
456+
},
457+
},
458+
440459
"workload_metadata_config": {
441460
Computed: true,
442461
Type: schema.TypeList,
@@ -880,8 +899,10 @@ func expandNodeConfig(v interface{}) *container.NodeConfig {
880899
Value: data["value"].(string),
881900
Effect: data["effect"].(string),
882901
}
902+
883903
nodeTaints = append(nodeTaints, taint)
884904
}
905+
885906
nc.Taints = nodeTaints
886907
}
887908

@@ -1071,13 +1092,24 @@ func flattenNodeConfigDefaults(c *container.NodeConfigDefaults) []map[string]int
10711092
return result
10721093
}
10731094

1074-
func flattenNodeConfig(c *container.NodeConfig) []map[string]interface{} {
1095+
// v == old state of `node_config`
1096+
func flattenNodeConfig(c *container.NodeConfig, v interface{}) []map[string]interface{} {
10751097
config := make([]map[string]interface{}, 0, 1)
10761098

10771099
if c == nil {
10781100
return config
10791101
}
10801102

1103+
// default to no prior taint state if there are any issues
1104+
oldTaints := []interface{}{}
1105+
oldNodeConfigSchemaContainer := v.([]interface{})
1106+
if len(oldNodeConfigSchemaContainer) != 0 {
1107+
oldNodeConfigSchema := oldNodeConfigSchemaContainer[0].(map[string]interface{})
1108+
if vt, ok := oldNodeConfigSchema["taint"]; ok && len(vt.([]interface{})) > 0 {
1109+
oldTaints = vt.([]interface{})
1110+
}
1111+
}
1112+
10811113
config = append(config, map[string]interface{}{
10821114
"machine_type": c.MachineType,
10831115
"disk_size_gb": c.DiskSizeGb,
@@ -1101,7 +1133,8 @@ func flattenNodeConfig(c *container.NodeConfig) []map[string]interface{} {
11011133
"spot": c.Spot,
11021134
"min_cpu_platform": c.MinCpuPlatform,
11031135
"shielded_instance_config": flattenShieldedInstanceConfig(c.ShieldedInstanceConfig),
1104-
"taint": flattenTaints(c.Taints),
1136+
"taint": flattenTaints(c.Taints, oldTaints),
1137+
"effective_taints": flattenEffectiveTaints(c.Taints),
11051138
"workload_metadata_config": flattenWorkloadMetadataConfig(c.WorkloadMetadataConfig),
11061139
"sandbox_config": flattenSandboxConfig(c.SandboxConfig),
11071140
"host_maintenance_policy": flattenHostMaintenancePolicy(c.HostMaintenancePolicy),
@@ -1241,7 +1274,31 @@ func flattenGKEReservationAffinity(c *container.ReservationAffinity) []map[strin
12411274
return result
12421275
}
12431276

1244-
func flattenTaints(c []*container.NodeTaint) []map[string]interface{} {
1277+
// flattenTaints records the set of taints already present in state.
1278+
func flattenTaints(c []*container.NodeTaint, oldTaints []interface{}) []map[string]interface{} {
1279+
taintKeys := map[string]struct{}{}
1280+
for _, raw := range oldTaints {
1281+
data := raw.(map[string]interface{})
1282+
taintKey := data["key"].(string)
1283+
taintKeys[taintKey] = struct{}{}
1284+
}
1285+
1286+
result := []map[string]interface{}{}
1287+
for _, taint := range c {
1288+
if _, ok := taintKeys[taint.Key]; ok {
1289+
result = append(result, map[string]interface{}{
1290+
"key": taint.Key,
1291+
"value": taint.Value,
1292+
"effect": taint.Effect,
1293+
})
1294+
}
1295+
}
1296+
1297+
return result
1298+
}
1299+
1300+
// flattenEffectiveTaints records the complete set of taints returned from GKE.
1301+
func flattenEffectiveTaints(c []*container.NodeTaint) []map[string]interface{} {
12451302
result := []map[string]interface{}{}
12461303
for _, taint := range c {
12471304
result = append(result, map[string]interface{}{
@@ -1250,6 +1307,7 @@ func flattenTaints(c []*container.NodeTaint) []map[string]interface{} {
12501307
"effect": taint.Effect,
12511308
})
12521309
}
1310+
12531311
return result
12541312
}
12551313

@@ -1319,74 +1377,6 @@ func containerNodePoolLabelsSuppress(k, old, new string, d *schema.ResourceData)
13191377
return true
13201378
}
13211379

1322-
func containerNodePoolTaintSuppress(k, old, new string, d *schema.ResourceData) bool {
1323-
// Node configs are embedded into multiple resources (container cluster and
1324-
// container node pool) so we determine the node config key dynamically.
1325-
idx := strings.Index(k, ".taint.")
1326-
if idx < 0 {
1327-
return false
1328-
}
1329-
1330-
root := k[:idx]
1331-
1332-
// Right now, GKE only applies its own out-of-band labels when you enable
1333-
// Sandbox. We only need to perform diff suppression in this case;
1334-
// otherwise, the default Terraform behavior is fine.
1335-
o, n := d.GetChange(root + ".sandbox_config.0.sandbox_type")
1336-
if o == nil || n == nil {
1337-
return false
1338-
}
1339-
1340-
// Pull the entire changeset as a list rather than trying to deal with each
1341-
// element individually.
1342-
o, n = d.GetChange(root + ".taint")
1343-
if o == nil || n == nil {
1344-
return false
1345-
}
1346-
1347-
type taintType struct {
1348-
Key, Value, Effect string
1349-
}
1350-
1351-
taintSet := make(map[taintType]struct{})
1352-
1353-
// Add all new taints to set.
1354-
for _, raw := range n.([]interface{}) {
1355-
data := raw.(map[string]interface{})
1356-
taint := taintType{
1357-
Key: data["key"].(string),
1358-
Value: data["value"].(string),
1359-
Effect: data["effect"].(string),
1360-
}
1361-
taintSet[taint] = struct{}{}
1362-
}
1363-
1364-
// Remove all current taints, skipping GKE-managed keys if not present in
1365-
// the new configuration.
1366-
for _, raw := range o.([]interface{}) {
1367-
data := raw.(map[string]interface{})
1368-
taint := taintType{
1369-
Key: data["key"].(string),
1370-
Value: data["value"].(string),
1371-
Effect: data["effect"].(string),
1372-
}
1373-
if _, ok := taintSet[taint]; ok {
1374-
delete(taintSet, taint)
1375-
} else if !strings.HasPrefix(taint.Key, "sandbox.gke.io/") && taint.Key != "kubernetes.io/arch" {
1376-
// User-provided taint removed in new configuration.
1377-
return false
1378-
}
1379-
}
1380-
1381-
// If, at this point, the set still has elements, the new configuration
1382-
// added an additional taint.
1383-
if len(taintSet) > 0 {
1384-
return false
1385-
}
1386-
1387-
return true
1388-
}
1389-
13901380
func flattenKubeletConfig(c *container.NodeKubeletConfig) []map[string]interface{} {
13911381
result := []map[string]interface{}{}
13921382
if c != nil {

google-beta/services/container/resource_container_cluster.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -2635,7 +2635,7 @@ func resourceContainerClusterRead(d *schema.ResourceData, meta interface{}) erro
26352635
return fmt.Errorf("Error setting default_max_pods_per_node: %s", err)
26362636
}
26372637
}
2638-
if err := d.Set("node_config", flattenNodeConfig(cluster.NodeConfig)); err != nil {
2638+
if err := d.Set("node_config", flattenNodeConfig(cluster.NodeConfig, d.Get("node_config"))); err != nil {
26392639
return err
26402640
}
26412641
if err := d.Set("project", project); err != nil {

google-beta/services/container/resource_container_cluster_test.go

+9-7
Original file line numberDiff line numberDiff line change
@@ -1222,17 +1222,19 @@ func TestAccContainerCluster_withNodeConfig(t *testing.T) {
12221222
Config: testAccContainerCluster_withNodeConfig(clusterName),
12231223
},
12241224
{
1225-
ResourceName: "google_container_cluster.with_node_config",
1226-
ImportState: true,
1227-
ImportStateVerify: true,
1225+
ResourceName: "google_container_cluster.with_node_config",
1226+
ImportState: true,
1227+
ImportStateVerify: true,
1228+
ImportStateVerifyIgnore: []string{"node_config.0.taint"},
12281229
},
12291230
{
12301231
Config: testAccContainerCluster_withNodeConfigUpdate(clusterName),
12311232
},
12321233
{
1233-
ResourceName: "google_container_cluster.with_node_config",
1234-
ImportState: true,
1235-
ImportStateVerify: true,
1234+
ResourceName: "google_container_cluster.with_node_config",
1235+
ImportState: true,
1236+
ImportStateVerify: true,
1237+
ImportStateVerifyIgnore: []string{"node_config.0.taint"},
12361238
},
12371239
},
12381240
})
@@ -1525,7 +1527,7 @@ func TestAccContainerCluster_withSandboxConfig(t *testing.T) {
15251527
ResourceName: "google_container_cluster.with_sandbox_config",
15261528
ImportState: true,
15271529
ImportStateVerify: true,
1528-
ImportStateVerifyIgnore: []string{"min_master_version"},
1530+
ImportStateVerifyIgnore: []string{"min_master_version", "node_config.0.taint"},
15291531
},
15301532
{
15311533
// GKE sets automatic labels and taints on nodes. This makes

google-beta/services/container/resource_container_node_pool.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -1106,7 +1106,7 @@ func flattenNodePool(d *schema.ResourceData, config *transport_tpg.Config, np *c
11061106
"initial_node_count": np.InitialNodeCount,
11071107
"node_locations": schema.NewSet(schema.HashString, tpgresource.ConvertStringArrToInterface(np.Locations)),
11081108
"node_count": nodeCount,
1109-
"node_config": flattenNodeConfig(np.Config),
1109+
"node_config": flattenNodeConfig(np.Config, d.Get(prefix+"node_config")),
11101110
"instance_group_urls": igmUrls,
11111111
"managed_instance_group_urls": managedIgmUrls,
11121112
"version": np.Version,

google-beta/services/container/resource_container_node_pool_test.go

+2-85
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ func TestAccContainerNodePool_withNodeConfig(t *testing.T) {
216216
ImportStateVerify: true,
217217
// autoscaling.# = 0 is equivalent to no autoscaling at all,
218218
// but will still cause an import diff
219-
ImportStateVerifyIgnore: []string{"autoscaling.#"},
219+
ImportStateVerifyIgnore: []string{"autoscaling.#", "node_config.0.taint"},
220220
},
221221
{
222222
Config: testAccContainerNodePool_withNodeConfigUpdate(cluster, nodePool),
@@ -227,7 +227,7 @@ func TestAccContainerNodePool_withNodeConfig(t *testing.T) {
227227
ImportStateVerify: true,
228228
// autoscaling.# = 0 is equivalent to no autoscaling at all,
229229
// but will still cause an import diff
230-
ImportStateVerifyIgnore: []string{"autoscaling.#"},
230+
ImportStateVerifyIgnore: []string{"autoscaling.#", "node_config.0.taint"},
231231
},
232232
},
233233
})
@@ -361,32 +361,6 @@ func TestAccContainerNodePool_withSandboxConfig(t *testing.T) {
361361
ImportState: true,
362362
ImportStateVerify: true,
363363
},
364-
{
365-
// GKE sets automatic labels and taints on nodes. This makes
366-
// sure we ignore the automatic ones and keep our own.
367-
Config: testAccContainerNodePool_withSandboxConfig(cluster, np),
368-
// When we use PlanOnly without ExpectNonEmptyPlan, we're
369-
// guaranteeing that the computed fields of the resources don't
370-
// force an unintentional change to the plan. That is, we
371-
// expect this part of the test to pass only if the plan
372-
// doesn't change.
373-
PlanOnly: true,
374-
},
375-
{
376-
// Now we'll modify the taints, which should force a change to
377-
// the plan. We make sure we don't over-suppress and end up
378-
// eliminating the labels or taints we asked for. This will
379-
// destroy and recreate the node pool as taints are immutable.
380-
Config: testAccContainerNodePool_withSandboxConfig_changeTaints(cluster, np),
381-
Check: resource.ComposeTestCheckFunc(
382-
resource.TestCheckResourceAttr("google_container_node_pool.with_sandbox_config",
383-
"node_config.0.labels.test.terraform.io/gke-sandbox", "true"),
384-
resource.TestCheckResourceAttr("google_container_node_pool.with_sandbox_config",
385-
"node_config.0.taint.0.key", "test.terraform.io/gke-sandbox"),
386-
resource.TestCheckResourceAttr("google_container_node_pool.with_sandbox_config",
387-
"node_config.0.taint.1.key", "test.terraform.io/gke-sandbox-amended"),
388-
),
389-
},
390364
},
391365
})
392366
}
@@ -2501,63 +2475,6 @@ resource "google_container_node_pool" "with_sandbox_config" {
25012475
"test.terraform.io/gke-sandbox" = "true"
25022476
}
25032477
2504-
taint {
2505-
key = "test.terraform.io/gke-sandbox"
2506-
value = "true"
2507-
effect = "NO_SCHEDULE"
2508-
}
2509-
2510-
oauth_scopes = [
2511-
"https://www.googleapis.com/auth/logging.write",
2512-
"https://www.googleapis.com/auth/monitoring",
2513-
]
2514-
}
2515-
}
2516-
`, cluster, np)
2517-
}
2518-
2519-
func testAccContainerNodePool_withSandboxConfig_changeTaints(cluster, np string) string {
2520-
return fmt.Sprintf(`
2521-
data "google_container_engine_versions" "central1a" {
2522-
location = "us-central1-a"
2523-
}
2524-
2525-
resource "google_container_cluster" "cluster" {
2526-
name = "%s"
2527-
location = "us-central1-a"
2528-
initial_node_count = 1
2529-
min_master_version = data.google_container_engine_versions.central1a.latest_master_version
2530-
}
2531-
2532-
resource "google_container_node_pool" "with_sandbox_config" {
2533-
name = "%s"
2534-
location = "us-central1-a"
2535-
cluster = google_container_cluster.cluster.name
2536-
initial_node_count = 1
2537-
node_config {
2538-
machine_type = "n1-standard-1" // can't be e2 because of gvisor
2539-
image_type = "COS_CONTAINERD"
2540-
2541-
sandbox_config {
2542-
sandbox_type = "gvisor"
2543-
}
2544-
2545-
labels = {
2546-
"test.terraform.io/gke-sandbox" = "true"
2547-
}
2548-
2549-
taint {
2550-
key = "test.terraform.io/gke-sandbox"
2551-
value = "true"
2552-
effect = "NO_SCHEDULE"
2553-
}
2554-
2555-
taint {
2556-
key = "test.terraform.io/gke-sandbox-amended"
2557-
value = "also-true"
2558-
effect = "NO_SCHEDULE"
2559-
}
2560-
25612478
oauth_scopes = [
25622479
"https://www.googleapis.com/auth/logging.write",
25632480
"https://www.googleapis.com/auth/monitoring",

0 commit comments

Comments
 (0)