Repository: cdk8s-team/cdk8s-plus
Pull request #1176 (2022-09-20 06:13:28)

feat(workload): `spread` property for easy `hostname` and `zone` spreading
Following Kubernetes [guidance](https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/#internal-default-constraints) to spread pods across nodes and zones for a workload. Adding a default spread property to workloads. 

Resolves https://github.com/cdk8s-team/cdk8s-plus/issues/890

Signed-off-by: Vinayak Kukreja <vinakuk@amazon.com>


Patch:
diff --git a/src/workload.ts b/src/workload.ts
index e6a50872..a6eff5b7 100644
--- a/src/workload.ts
+++ b/src/workload.ts
@@ -23,6 +23,13 @@ export interface WorkloadProps extends pod.AbstractPodProps {
    */
   readonly select?: boolean;
 
+  /**
+   * Automatically spread pods across hostname and zones.
+   *
+   * @see https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/#internal-default-constraints
+   * @default false
+   */
+  readonly spread?: boolean;
 }
 
 /**
@@ -64,6 +71,8 @@ export abstract class Workload extends pod.AbstractPod {
 
   public readonly scheduling: WorkloadScheduling;
 
+  private readonly spread: boolean;
+
   private readonly _matchLabels: Record<string, string> = {};
   private readonly _matchExpressions: LabelSelectorRequirement[] = [];
 
@@ -73,6 +82,7 @@ export abstract class Workload extends pod.AbstractPod {
     this.podMetadata = new ApiObjectMetadataDefinition(props.podMetadata);
     this.scheduling = new WorkloadScheduling(this);
     this.connections = new pod.PodConnections(this);
+    this.spread = props.spread ?? false;
 
     const matcher = Names.toLabelValue(this);
     this.podMetadata.addLabel(pod.Pod.ADDRESS_LABEL, matcher);
@@ -80,7 +90,6 @@ export abstract class Workload extends pod.AbstractPod {
     if (props.select ?? true) {
       this.select(pod.LabelSelector.of({ labels: { [pod.Pod.ADDRESS_LABEL]: matcher } }));
     }
-
   }
 
   /**
@@ -128,6 +137,17 @@ export abstract class Workload extends pod.AbstractPod {
    * @internal
    */
   public _toPodSpec(): k8s.PodSpec {
+    if (this.spread) {
+      {
+        this.scheduling.spread({
+          topology: pod.Topology.HOSTNAME,
+        });
+        this.scheduling.spread({
+          topology: pod.Topology.ZONE,
+        });
+      }
+    };
+
     const scheduling = this.scheduling._toKube();
     return {
       ...super._toPodSpec(),


Test patch:
diff --git a/test/__snapshots__/deployment.test.ts.snap b/test/__snapshots__/deployment.test.ts.snap
index a8163ef0..20e5e40e 100644
--- a/test/__snapshots__/deployment.test.ts.snap
+++ b/test/__snapshots__/deployment.test.ts.snap
@@ -1821,3 +1821,96 @@ Array [
   },
 ]
 `;
+
+exports[`scheduling spread set to true 1`] = `
+Array [
+  Object {
+    "apiVersion": "apps/v1",
+    "kind": "Deployment",
+    "metadata": Object {
+      "name": "test-deployment-c898c72d",
+    },
+    "spec": Object {
+      "minReadySeconds": 0,
+      "progressDeadlineSeconds": 600,
+      "replicas": 2,
+      "selector": Object {
+        "matchLabels": Object {
+          "cdk8s.io/metadata.addr": "test-Deployment-c83f5e59",
+        },
+      },
+      "strategy": Object {
+        "rollingUpdate": Object {
+          "maxSurge": "25%",
+          "maxUnavailable": "25%",
+        },
+        "type": "RollingUpdate",
+      },
+      "template": Object {
+        "metadata": Object {
+          "labels": Object {
+            "cdk8s.io/metadata.addr": "test-Deployment-c83f5e59",
+          },
+        },
+        "spec": Object {
+          "affinity": Object {
+            "podAntiAffinity": Object {
+              "requiredDuringSchedulingIgnoredDuringExecution": Array [
+                Object {
+                  "labelSelector": Object {
+                    "matchLabels": Object {
+                      "cdk8s.io/metadata.addr": "test-Deployment-c83f5e59",
+                    },
+                  },
+                  "topologyKey": "kubernetes.io/hostname",
+                },
+                Object {
+                  "labelSelector": Object {
+                    "matchLabels": Object {
+                      "cdk8s.io/metadata.addr": "test-Deployment-c83f5e59",
+                    },
+                  },
+                  "topologyKey": "topology.kubernetes.io/zone",
+                },
+              ],
+            },
+          },
+          "automountServiceAccountToken": false,
+          "containers": Array [
+            Object {
+              "image": "redis",
+              "imagePullPolicy": "Always",
+              "name": "main",
+              "resources": Object {
+                "limits": Object {
+                  "cpu": "1500m",
+                  "memory": "2048Mi",
+                },
+                "requests": Object {
+                  "cpu": "1000m",
+                  "memory": "512Mi",
+                },
+              },
+              "securityContext": Object {
+                "allowPrivilegeEscalation": false,
+                "privileged": false,
+                "readOnlyRootFilesystem": true,
+                "runAsGroup": 26000,
+                "runAsNonRoot": true,
+                "runAsUser": 25000,
+              },
+            },
+          ],
+          "dnsPolicy": "ClusterFirst",
+          "restartPolicy": "Always",
+          "securityContext": Object {
+            "fsGroupChangePolicy": "Always",
+            "runAsNonRoot": true,
+          },
+          "setHostnameAsFQDN": false,
+        },
+      },
+    },
+  },
+]
+`;
diff --git a/test/deployment.test.ts b/test/deployment.test.ts
index 6bb91155..b4e015ac 100644
--- a/test/deployment.test.ts
+++ b/test/deployment.test.ts
@@ -536,6 +536,18 @@ describe('scheduling', () => {
 
   });
 
+  test('spread set to true', () => {
+
+    const chart = Testing.chart();
+
+    new kplus.Deployment(chart, 'Deployment', {
+      containers: [{ image: 'redis' }],
+      spread: true,
+    });
+
+    expect(Testing.synth(chart)).toMatchSnapshot();
+  });
+
   test('can be separated from a managed deployment - default', () => {
 
     const chart = Testing.chart();