Repository: cdk8s-team/cdk8s-plus
Pull request #3812 (2024-03-01 07:36:09)

feat(container): support `restartPolicy` field
[KEP-753](https://github.com/kubernetes/enhancements/tree/e1a916745378660bafb96cb3c9f754fa793342f9/keps/sig-node/753-sidecar-containers) introduced native sidecar container support starting from kubernetes v1.28

This PR adds the field to Container object. Existing tests are working, I added one more test to make sure field is there if the property is specified

serhatcetinkaya: @iliapolo thanks for the detailed check, I added all your suggestions. It should be ok now, can you give it another look please?

Patch:
diff --git a/src/container.ts b/src/container.ts
index 28048a6e..e5940b92 100644
--- a/src/container.ts
+++ b/src/container.ts
@@ -491,6 +491,27 @@ export interface ContainerLifecycle {
 
 }
 
+/**
+ * RestartPolicy defines the restart behavior of individual containers in a pod.
+ * This field may only be set for init containers, and the only allowed value is "Always".
+ * For non-init containers or when this field is not specified,
+ * the restart behavior is defined by the Pod's restart policy and the container type.
+ * Setting the RestartPolicy as "Always" for the init container will have the following effect:
+ * this init container will be continually restarted on exit until all regular containers have terminated.
+ * Once all regular containers have completed, all init containers with restartPolicy "Always" will be shut down.
+ * This lifecycle differs from normal init containers and is often referred to as a "sidecar" container.
+ *
+ * @see https://kubernetes.io/docs/concepts/workloads/pods/sidecar-containers/
+ */
+export enum ContainerRestartPolicy {
+
+  /**
+   * If an init container is created with its restartPolicy set to Always,
+   * it will start and remain running during the entire life of the Pod.
+   * For regular containers, this is ignored by Kubernetes.
+   */
+  ALWAYS = 'Always'
+}
 /**
  * Properties for creating a container.
  */
@@ -658,6 +679,15 @@ export interface ContainerOpts {
    *   group: 26000
    */
   readonly securityContext?: ContainerSecurityContextProps;
+
+  /**
+   * Kubelet will start init containers with restartPolicy=Always in the order with other init containers,
+   * but instead of waiting for its completion, it will wait for the container startup completion
+   * Currently, only accepted value is Always
+   * @see https://kubernetes.io/docs/concepts/workloads/pods/sidecar-containers/
+   * @default - no restart policy is defined and the pod restart policy is applied
+   */
+  readonly restartPolicy?: ContainerRestartPolicy;
 }
 
 /**
@@ -713,6 +743,11 @@ export class Container {
    */
   public readonly env: Env;
 
+  /**
+   * The restart policy of the container.
+   */
+  public readonly restartPolicy?: ContainerRestartPolicy;
+
   private readonly _command?: readonly string[];
   private readonly _args?: readonly string[];
   private readonly _ports: ContainerPort[] = [];
@@ -759,6 +794,7 @@ export class Container {
     this.imagePullPolicy = props.imagePullPolicy ?? ImagePullPolicy.ALWAYS;
     this.securityContext = new ContainerSecurityContext(props.securityContext);
     this.env = new Env(props.envFrom ?? [], props.envVariables ?? {});
+    this.restartPolicy = props.restartPolicy;
 
     if (this.portNumber) {
       this.addPort({
@@ -922,6 +958,7 @@ export class Container {
         preStop: this._lifecycle.preStop?._toKube(this),
       } : undefined,
       resources: resourceRequirements,
+      restartPolicy: this.restartPolicy,
       securityContext: this.securityContext._toKube(),
     };
   }
diff --git a/src/pod.ts b/src/pod.ts
index db6ea332..c7f4ea6b 100644
--- a/src/pod.ts
+++ b/src/pod.ts
@@ -190,6 +190,10 @@ export abstract class AbstractPod extends base.Resource implements IPodSelector,
     const initContainers: k8s.Container[] = [];
 
     for (const cont of this.containers) {
+      // check if restartPolicy is defined for containers
+      if (cont.restartPolicy) {
+        throw new Error(`Invalid container spec: ${cont.name} has non-empty restartPolicy field. The field can only be specified for initContainers`);
+      }
       // automatically add volume from the container mount
       // to this pod so thats its available to the container.
       for (const mount of cont.mounts) {


Test patch:
diff --git a/test/__snapshots__/container.test.ts.snap b/test/__snapshots__/container.test.ts.snap
index 5271e331..c94d5044 100644
--- a/test/__snapshots__/container.test.ts.snap
+++ b/test/__snapshots__/container.test.ts.snap
@@ -1,5 +1,79 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`Container "restartPolicy" property can be used to define restartPolicy 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": false,
+      "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 {
+            "allowPrivilegeEscalation": false,
+            "privileged": false,
+            "readOnlyRootFilesystem": true,
+            "runAsNonRoot": true,
+          },
+        },
+      ],
+      "dnsPolicy": "ClusterFirst",
+      "hostNetwork": false,
+      "initContainers": Array [
+        Object {
+          "image": "bar",
+          "imagePullPolicy": "Always",
+          "name": "init-0",
+          "resources": Object {
+            "limits": Object {
+              "cpu": "1500m",
+              "memory": "2048Mi",
+            },
+            "requests": Object {
+              "cpu": "1000m",
+              "memory": "512Mi",
+            },
+          },
+          "restartPolicy": "Always",
+          "securityContext": Object {
+            "allowPrivilegeEscalation": false,
+            "privileged": false,
+            "readOnlyRootFilesystem": true,
+            "runAsNonRoot": true,
+          },
+        },
+      ],
+      "restartPolicy": "Always",
+      "securityContext": Object {
+        "fsGroupChangePolicy": "Always",
+        "runAsNonRoot": true,
+      },
+      "setHostnameAsFQDN": false,
+      "terminationGracePeriodSeconds": 30,
+    },
+  },
+]
+`;
+
 exports[`Container "startupProbe" property has defaults if port is provided 1`] = `
 Array [
   Object {
diff --git a/test/container.test.ts b/test/container.test.ts
index eccb9ba7..4d570d58 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, Probe, k8s } from '../src';
+import { Container, Cpu, Handler, ConnectionScheme, Probe, k8s, ContainerRestartPolicy } from '../src';
 
 describe('EnvValue', () => {
 
@@ -536,6 +536,26 @@ describe('Container', () => {
     expect(container).not.toHaveProperty('startupProbe');
   });
 
+  test('"restartPolicy" property can be used to define restartPolicy', () => {
+    // GIVEN
+    const chart = Testing.chart();
+    const pod = new kplus.Pod(chart, 'Pod');
+
+    // WHEN
+    pod.addContainer({ image: 'foo' });
+    pod.addInitContainer({
+      image: 'bar',
+      restartPolicy: ContainerRestartPolicy.ALWAYS,
+    });
+
+    // THEN
+    const manifest = Testing.synth(chart);
+    expect(manifest).toMatchSnapshot();
+    const container = manifest[0].spec.initContainers[0];
+
+    expect(container.restartPolicy).toEqual('Always');
+  });
+
   test('"readiness", "liveness", and "startup" can be used to define probes', () => {
     // GIVEN
     const container = new kplus.Container({
diff --git a/test/pod.test.ts b/test/pod.test.ts
index 9378cb28..977e64da 100644
--- a/test/pod.test.ts
+++ b/test/pod.test.ts
@@ -1,7 +1,7 @@
 import { Testing, ApiObject, Duration } from 'cdk8s';
 import { Node } from 'constructs';
 import * as kplus from '../src';
-import { DockerConfigSecret, FsGroupChangePolicy, Probe, k8s, RestartPolicy } from '../src';
+import { DockerConfigSecret, FsGroupChangePolicy, Probe, k8s, RestartPolicy, ContainerRestartPolicy } from '../src';
 
 test('defaults', () => {
 
@@ -1556,4 +1556,13 @@ test('custom termination grace period - minutes', () => {
   const manifest = Testing.synth(chart);
   const spec = manifest[0].spec;
   expect(spec.terminationGracePeriodSeconds).toEqual(120);
-});
\ No newline at end of file
+});
+
+test('Containers should not specify "restartPolicy" field', () => {
+  const chart = Testing.chart();
+  new kplus.Pod(chart, 'Pod', {
+    containers: [{ image: 'image', restartPolicy: ContainerRestartPolicy.ALWAYS }],
+  });
+  expect(() => Testing.synth(chart)).toThrowError();
+});
+