Repository: awslabs/cdk8s
Pull request #355 (2020-10-21 06:28:40)

feat: chart-level labels
Allow specifying `labels` when defining charts and those will be applied to all API objects within this chart.

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*


mergify[bot]: Your pull request will be updated and merged automatically (do not update manually).

Patch:
diff --git a/packages/cdk8s/API.md b/packages/cdk8s/API.md
index 29b01c19d..4e26db2a5 100644
--- a/packages/cdk8s/API.md
+++ b/packages/cdk8s/API.md
@@ -273,6 +273,7 @@ new Chart(scope: Construct, ns: string, options?: ChartOptions)
 * **scope** (<code>[Construct](#constructs-construct)</code>)  *No description*
 * **ns** (<code>string</code>)  *No description*
 * **options** (<code>[ChartOptions](#cdk8s-chartoptions)</code>)  *No description*
+  * **labels** (<code>Map<string, string></code>)  Labels to apply to all resources in this chart. __*Default*__: no common labels
   * **namespace** (<code>string</code>)  The default namespace for all objects defined in this chart (directly or indirectly). __*Default*__: no namespace is synthesized (usually this implies "default")
 
 
@@ -282,6 +283,7 @@ new Chart(scope: Construct, ns: string, options?: ChartOptions)
 
 Name | Type | Description 
 -----|------|-------------
+**labels**🔹 | <code>Map<string, string></code> | Labels applied to all resources in this chart.
 **namespace**?🔹 | <code>string</code> | The default namespace for all objects in this chart.<br/>__*Optional*__
 
 ### Methods
@@ -802,6 +804,7 @@ Name | Type | Description
 
 Name | Type | Description 
 -----|------|-------------
+**labels**?🔹 | <code>Map<string, string></code> | Labels to apply to all resources in this chart.<br/>__*Default*__: no common labels
 **namespace**?🔹 | <code>string</code> | The default namespace for all objects defined in this chart (directly or indirectly).<br/>__*Default*__: no namespace is synthesized (usually this implies "default")
 
 
diff --git a/packages/cdk8s/README.md b/packages/cdk8s/README.md
index a54701db7..fd008f1da 100644
--- a/packages/cdk8s/README.md
+++ b/packages/cdk8s/README.md
@@ -28,6 +28,38 @@ class MyChart extends Chart {
 During synthesis, charts collect all the `ApiObject` nodes (recursively) and
 emit a single YAML manifest that includes all these objects.
 
+When a chart is defined, you can specify chart-level `namespace` and `labels`.
+Those will be applied to all API objects defined within the chart (recursively):
+
+```ts
+class MyChart extends Chart {
+  constructor(scope: Construct, ns: string) {
+    super(scope, ns, {
+      namespace: 'my-namespace',
+      labels: {
+        app: 'my-app',
+      },
+    });
+
+    new ApiObject(this, 'my-object', {
+      apiVersion: 'v1',
+      kind: 'Foo'
+    });
+  }
+}
+```
+
+Will synthesize into:
+
+```yaml
+apiVersion: v1
+kind: Foo
+metadata:
+  namesapce: my-namespace
+  labels:
+    app: my-app
+```
+
 ## ApiObject
 
 An `ApiObject` is a construct that represents an entry in a Kubernetes manifest (level 0).
@@ -241,8 +273,8 @@ The `Helm` construct will render the manifest from the specified chart by
 executing `helm template`. If `values` is specified, these values will override
 the default values included with the chart.
 
-The `name` option can be used to specify the chart's [release name](https://helm.sh/docs/intro/using_helm/#three-big-concepts). 
-If not specified, a valid and unique release name will be allocated 
+The `name` option can be used to specify the chart's [release name](https://helm.sh/docs/intro/using_helm/#three-big-concepts).
+If not specified, a valid and unique release name will be allocated
 based on the construct path.
 
 The `Helm` construct extends `Include` and inherits it's API. For example, you
diff --git a/packages/cdk8s/src/api-object.ts b/packages/cdk8s/src/api-object.ts
index 174508858..5a2eaa12e 100644
--- a/packages/cdk8s/src/api-object.ts
+++ b/packages/cdk8s/src/api-object.ts
@@ -87,10 +87,15 @@ export class ApiObject extends Construct {
 
     this.metadata = new ApiObjectMetadataDefinition({
       name: this.name,
-      namespace: options.metadata?.namespace ?? this.chart.namespace,
 
-      // override user defined values
+      // user defined values
       ...options.metadata,
+      
+      namespace: options.metadata?.namespace ?? this.chart.namespace,
+      labels: {
+        ...this.chart.labels,
+        ...options.metadata?.labels,
+      },
     });
   }
 
diff --git a/packages/cdk8s/src/chart.ts b/packages/cdk8s/src/chart.ts
index 50e296731..887dc393f 100644
--- a/packages/cdk8s/src/chart.ts
+++ b/packages/cdk8s/src/chart.ts
@@ -12,6 +12,13 @@ export interface ChartOptions {
    * @default - no namespace is synthesized (usually this implies "default")
    */
   readonly namespace?: string;
+
+  /**
+   * Labels to apply to all resources in this chart.
+   * 
+   * @default - no common labels
+   */
+  readonly labels?: { [name: string]: string };
 }
 
 export class Chart extends Construct {
@@ -38,9 +45,24 @@ export class Chart extends Construct {
    */
   public readonly namespace?: string;
 
+  /**
+   * Chart-level labels.
+   */
+  private readonly _labels?: { [name: string]: string };
+
   constructor(scope: Construct, ns: string, options: ChartOptions = { }) {
     super(scope, ns);
     this.namespace = options.namespace;
+    this._labels = options.labels ?? {};
+  }
+
+  /**
+   * Labels applied to all resources in this chart.
+   * 
+   * This is an immutable copy.
+   */
+  public get labels(): { [name: string]: string } {
+    return { ...this._labels };
   }
 
   /**
@@ -84,6 +106,5 @@ export class Chart extends Construct {
   public toJson(): any[] {
     return App._synthChart(this);
   }
-
 }
 


Test patch:
diff --git a/packages/cdk8s/test/api-object.test.ts b/packages/cdk8s/test/api-object.test.ts
index 55915f008..3ae1aff14 100644
--- a/packages/cdk8s/test/api-object.test.ts
+++ b/packages/cdk8s/test/api-object.test.ts
@@ -191,4 +191,57 @@ test('default namespace can be defined at the chart level', () => {
       namespace: 'foobar',
     },
   }]);
+});
+
+test('chart labels are applied to all api objects in the chart', () => {
+  // GIVEN
+  const app = Testing.app();
+
+  // WHEN
+  const chart = new Chart(app, 'my-chart', {
+    labels: {
+      foo: 'ffffffffff',
+      bar: 'bbbbbb',
+    },
+  });
+
+  new ApiObject(chart, 'obj1', { kind: 'Obj1', apiVersion: 'v1' });
+  const group = new Construct(chart, 'group');
+  new ApiObject(group, 'obj2', {
+    kind: 'Obj2',
+    apiVersion: 'v2',
+    metadata: {
+      labels: {
+        foo: 'override by object',
+        zoo: 'zoo1',
+      },
+    },
+  });
+
+  // THEN
+  expect(Testing.synth(chart)).toEqual([
+    {
+      apiVersion: 'v1',
+      kind: 'Obj1',
+      metadata: {
+        labels: {
+          bar: 'bbbbbb',
+          foo: 'ffffffffff',
+        },
+        name: 'my-chart-obj1-01c0df67',
+      },
+    },
+    {
+      apiVersion: 'v2',
+      kind: 'Obj2',
+      metadata: {
+        labels: {
+          bar: 'bbbbbb',
+          foo: 'override by object',
+          zoo: 'zoo1',  
+        },
+        name: 'my-chart-group-obj2-ba1eb578',
+      },
+    },
+  ]);
 });
\ No newline at end of file