Repository: cdk8s-team/cdk8s-plus
Pull request #1124 (2022-09-11 07:05:24)

feat(container): default container startup probe (#1093)
# Backport

This will backport the following commits from `k8s-24/main` to `k8s-23/main`:
 - [feat(container): default container startup probe (#1093)](https://github.com/cdk8s-team/cdk8s-plus/pull/1093)

<!--- Backport version: 8.9.3 -->

### Questions ?
Please refer to the [Backport tool documentation](https://github.com/sqren/backport)


Patch:
diff --git a/src/container.ts b/src/container.ts
index 5795b294..fbd8df8e 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 150ee4c5..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: k8s.IoK8SApiCoreV1ContainerImagePullPolicy.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({