Repository: cdk8s-team/cdk8s-plus
Pull request #1093 (2022-09-02 05:31:40)

feat(container): default container startup probe
Startup probe help Kubernetes determine if the container is ready and no other probe is initiated till this check is complete. Currently we leave it as undefined in the manifest if the values are not provided.
Now, if a port number is provided by the user, that will be knocked on to understand status of the container.

Resolves https://github.com/cdk8s-team/cdk8s-plus/issues/808

Signed-off-by: Vinayak Kukreja <vinakuk@amazon.com>


Patch:
diff --git a/src/container.ts b/src/container.ts
index c556132d..13164576 100644
--- a/src/container.ts
+++ b/src/container.ts
@@ -603,7 +603,10 @@ export interface ContainerProps {
    * StartupProbe indicates that the Pod has successfully initialized.
    * If specified, no other probes are executed until this completes successfully
    *
-   * @default - no startup probe is defined.
+   * @default - If a port is provided, then knocks on that port
+   * to determine when the container is ready for readiness and
+   * liveness probe checks.
+   * Otherwise, no startup probe is defined.
    */
   readonly startup?: probe.Probe;
 
@@ -721,14 +724,17 @@ export class Container {
       throw new Error('Either \'port\' or \'portNumber\' is allowed. Use \'portNumber\' since \'port\' is deprecated');
     }
 
+    const portNumber = props.portNumber ?? props.port;
+    const defaultProbeConfiguration: probe.Probe = probe.Probe.fromTcpSocket({ port: portNumber });
+
     this.name = props.name ?? 'main';
     this.image = props.image;
-    this.portNumber = props.portNumber ?? props.port;
+    this.portNumber = portNumber;
     this._command = props.command;
     this._args = props.args;
     this._readiness = props.readiness;
     this._liveness = props.liveness;
-    this._startup = props.startup;
+    this._startup = props.startup ?? (this.portNumber ? defaultProbeConfiguration : undefined);
     this._lifecycle = props.lifecycle;
     this.resources = props.resources ?? defaultResourceSpec;
     this.workingDir = props.workingDir;


Test patch:
diff --git a/test/__snapshots__/container.test.ts.snap b/test/__snapshots__/container.test.ts.snap
index 631338b9..2cc55332 100644
--- a/test/__snapshots__/container.test.ts.snap
+++ b/test/__snapshots__/container.test.ts.snap
@@ -1,5 +1,181 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`Container "startupProbe" property has defaults if port is provided 1`] = `
+Array [
+  Object {
+    "apiVersion": "v1",
+    "kind": "Pod",
+    "metadata": Object {
+      "labels": Object {
+        "cdk8s.io/metadata.addr": "test-Pod-c815bc91",
+      },
+      "name": "test-pod-c890e1b8",
+    },
+    "spec": Object {
+      "automountServiceAccountToken": true,
+      "containers": Array [
+        Object {
+          "image": "foo",
+          "imagePullPolicy": "Always",
+          "name": "main",
+          "ports": Array [
+            Object {
+              "containerPort": 8080,
+            },
+          ],
+          "resources": Object {
+            "limits": Object {
+              "cpu": "1500m",
+              "memory": "2048Mi",
+            },
+            "requests": Object {
+              "cpu": "1000m",
+              "memory": "512Mi",
+            },
+          },
+          "securityContext": Object {
+            "privileged": false,
+            "readOnlyRootFilesystem": false,
+            "runAsGroup": 26000,
+            "runAsNonRoot": false,
+            "runAsUser": 25000,
+          },
+          "startupProbe": Object {
+            "failureThreshold": 3,
+            "tcpSocket": Object {
+              "port": 8080,
+            },
+          },
+        },
+      ],
+      "dnsPolicy": "ClusterFirst",
+      "securityContext": Object {
+        "fsGroupChangePolicy": "Always",
+        "runAsNonRoot": false,
+      },
+      "setHostnameAsFQDN": false,
+    },
+  },
+]
+`;
+
+exports[`Container "startupProbe" property is undefined if port is not provided 1`] = `
+Array [
+  Object {
+    "apiVersion": "v1",
+    "kind": "Pod",
+    "metadata": Object {
+      "labels": Object {
+        "cdk8s.io/metadata.addr": "test-Pod-c815bc91",
+      },
+      "name": "test-pod-c890e1b8",
+    },
+    "spec": Object {
+      "automountServiceAccountToken": true,
+      "containers": Array [
+        Object {
+          "image": "foo",
+          "imagePullPolicy": "Always",
+          "name": "main",
+          "resources": Object {
+            "limits": Object {
+              "cpu": "1500m",
+              "memory": "2048Mi",
+            },
+            "requests": Object {
+              "cpu": "1000m",
+              "memory": "512Mi",
+            },
+          },
+          "securityContext": Object {
+            "privileged": false,
+            "readOnlyRootFilesystem": false,
+            "runAsGroup": 26000,
+            "runAsNonRoot": false,
+            "runAsUser": 25000,
+          },
+        },
+      ],
+      "dnsPolicy": "ClusterFirst",
+      "securityContext": Object {
+        "fsGroupChangePolicy": "Always",
+        "runAsNonRoot": false,
+      },
+      "setHostnameAsFQDN": false,
+    },
+  },
+]
+`;
+
+exports[`Container Instantiation properties are all respected 1`] = `
+Array [
+  Object {
+    "apiVersion": "v1",
+    "kind": "Pod",
+    "metadata": Object {
+      "labels": Object {
+        "cdk8s.io/metadata.addr": "test-Pod-c815bc91",
+      },
+      "name": "test-pod-c890e1b8",
+    },
+    "spec": Object {
+      "automountServiceAccountToken": true,
+      "containers": Array [
+        Object {
+          "command": Array [
+            "command",
+          ],
+          "env": Array [
+            Object {
+              "name": "key",
+              "value": "value",
+            },
+          ],
+          "image": "image",
+          "imagePullPolicy": "Never",
+          "name": "name",
+          "ports": Array [
+            Object {
+              "containerPort": 9000,
+            },
+          ],
+          "resources": Object {
+            "limits": Object {
+              "cpu": "1500m",
+              "memory": "2048Mi",
+            },
+            "requests": Object {
+              "cpu": "1000m",
+              "memory": "512Mi",
+            },
+          },
+          "securityContext": Object {
+            "privileged": false,
+            "readOnlyRootFilesystem": false,
+            "runAsGroup": 26000,
+            "runAsNonRoot": false,
+            "runAsUser": 25000,
+          },
+          "startupProbe": Object {
+            "failureThreshold": 3,
+            "tcpSocket": Object {
+              "port": 9000,
+            },
+          },
+          "workingDir": "workingDir",
+        },
+      ],
+      "dnsPolicy": "ClusterFirst",
+      "securityContext": Object {
+        "fsGroupChangePolicy": "Always",
+        "runAsNonRoot": false,
+      },
+      "setHostnameAsFQDN": false,
+    },
+  },
+]
+`;
+
 exports[`Container can mount container to a pv 1`] = `
 Array [
   Object {
diff --git a/test/__snapshots__/deployment.test.ts.snap b/test/__snapshots__/deployment.test.ts.snap
index f7fcecfb..87b99cf7 100644
--- a/test/__snapshots__/deployment.test.ts.snap
+++ b/test/__snapshots__/deployment.test.ts.snap
@@ -59,6 +59,12 @@ Array [
                 "runAsNonRoot": false,
                 "runAsUser": 25000,
               },
+              "startupProbe": Object {
+                "failureThreshold": 3,
+                "tcpSocket": Object {
+                  "port": 9300,
+                },
+              },
             },
           ],
           "dnsPolicy": "ClusterFirst",
@@ -284,6 +290,12 @@ Array [
                 "runAsNonRoot": false,
                 "runAsUser": 25000,
               },
+              "startupProbe": Object {
+                "failureThreshold": 3,
+                "tcpSocket": Object {
+                  "port": 9300,
+                },
+              },
             },
           ],
           "dnsPolicy": "ClusterFirst",
diff --git a/test/__snapshots__/pod.test.ts.snap b/test/__snapshots__/pod.test.ts.snap
index b9540be6..6836cb09 100644
--- a/test/__snapshots__/pod.test.ts.snap
+++ b/test/__snapshots__/pod.test.ts.snap
@@ -169,6 +169,12 @@ Array [
             "runAsNonRoot": false,
             "runAsUser": 25000,
           },
+          "startupProbe": Object {
+            "failureThreshold": 3,
+            "tcpSocket": Object {
+              "port": 6739,
+            },
+          },
         },
       ],
       "dnsPolicy": "ClusterFirst",
@@ -581,6 +587,12 @@ Array [
             "runAsNonRoot": false,
             "runAsUser": 25000,
           },
+          "startupProbe": Object {
+            "failureThreshold": 3,
+            "tcpSocket": Object {
+              "port": 6739,
+            },
+          },
         },
       ],
       "dnsPolicy": "ClusterFirst",
diff --git a/test/container.test.ts b/test/container.test.ts
index 00a487ff..704ecc0f 100644
--- a/test/container.test.ts
+++ b/test/container.test.ts
@@ -1,7 +1,7 @@
 import * as cdk8s from 'cdk8s';
 import { Size, Testing } from 'cdk8s';
 import * as kplus from '../src';
-import { Container, Cpu, Handler, ConnectionScheme } from '../src';
+import { Container, Cpu, Handler, ConnectionScheme, Probe } from '../src';
 import * as k8s from '../src/imports/k8s';
 
 describe('EnvValue', () => {
@@ -228,7 +228,16 @@ describe('Container', () => {
 
   test('Instantiation properties are all respected', () => {
 
-    const container = new kplus.Container({
+    // GIVEN
+    const chart = Testing.chart();
+    const pod = new kplus.Pod(chart, 'Pod');
+    const port = 9000;
+    const startup = Probe.fromTcpSocket({
+      port: port,
+    });
+
+    // WHEN
+    pod.addContainer({
       image: 'image',
       name: 'name',
       imagePullPolicy: kplus.ImagePullPolicy.NEVER,
@@ -238,44 +247,27 @@ describe('Container', () => {
       envVariables: {
         key: kplus.EnvValue.fromValue('value'),
       },
+      startup: startup,
     });
 
-    const actual: k8s.Container = container._toKube();
-
-    const expected: k8s.Container = {
-      name: 'name',
-      imagePullPolicy: 'Never',
-      image: 'image',
-      workingDir: 'workingDir',
-      ports: [{
-        containerPort: 9000,
-      }],
-      resources: {
-        limits: {
-          cpu: k8s.Quantity.fromString('1500m'),
-          memory: k8s.Quantity.fromString('2048Mi'),
-        },
-        requests: {
-          cpu: k8s.Quantity.fromString('1000m'),
-          memory: k8s.Quantity.fromString('512Mi'),
-        },
-      },
-      command: ['command'],
-      env: [{
-        name: 'key',
-        value: 'value',
-      }],
-      securityContext: {
-        privileged: false,
-        readOnlyRootFilesystem: false,
-        runAsNonRoot: false,
-        runAsUser: 25000,
-        runAsGroup: 26000,
-      },
-    };
-
-    expect(actual).toEqual(expected);
-
+    // THEN
+    const manifest = Testing.synth(chart);
+    expect(manifest).toMatchSnapshot();
+    const container = manifest[0].spec.containers[0];
+
+    expect(container.name).toEqual('name');
+    expect(container.imagePullPolicy).toEqual('Never');
+    expect(container.image).toEqual('image');
+    expect(container.workingDir).toEqual('workingDir');
+    expect(container.ports[0].containerPort).toEqual(9000);
+    expect(container.command[0]).toEqual('command');
+    expect(container.env[0].name).toEqual('key');
+    expect(container.env[0].value).toEqual('value');
+    expect(container.securityContext.privileged).toEqual(false);
+    expect(container.securityContext.readOnlyRootFilesystem).toEqual(false);
+    expect(container.securityContext.runAsNonRoot).toEqual(false);
+    expect(container.startupProbe.failureThreshold).toEqual(3);
+    expect(container.startupProbe.tcpSocket.port).toEqual(9000);
   });
 
   test('Must use container props', () => {
@@ -415,6 +407,43 @@ describe('Container', () => {
     expect(container._toKube().volumeMounts).toEqual([expected]);
   });
 
+  test('"startupProbe" property has defaults if port is provided', () => {
+    // GIVEN
+    const chart = Testing.chart();
+    const pod = new kplus.Pod(chart, 'Pod');
+    const port = 8080;
+
+    // WHEN
+    pod.addContainer({
+      image: 'foo',
+      port: port,
+    });
+
+    // THEN
+    const manifest = Testing.synth(chart);
+    expect(manifest).toMatchSnapshot();
+    const container = manifest[0].spec.containers[0];
+
+    expect(container.startupProbe.failureThreshold).toEqual(3);
+    expect(container.startupProbe.tcpSocket.port).toEqual(8080);
+  });
+
+  test('"startupProbe" property is undefined if port is not provided', () => {
+    // GIVEN
+    const chart = Testing.chart();
+    const pod = new kplus.Pod(chart, 'Pod');
+
+    // WHEN
+    pod.addContainer({ image: 'foo' });
+
+    // THEN
+    const manifest = Testing.synth(chart);
+    expect(manifest).toMatchSnapshot();
+    const container = manifest[0].spec.containers[0];
+
+    expect(container).not.toHaveProperty('startupProbe');
+  });
+
   test('"readiness", "liveness", and "startup" can be used to define probes', () => {
     // GIVEN
     const container = new kplus.Container({