kubernetes-fluent-client 0.0.0-development → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +15 -0
  2. package/__mocks__/@kubernetes/client-node.ts +22 -0
  3. package/dist/fetch.d.ts +23 -0
  4. package/dist/fetch.d.ts.map +1 -0
  5. package/dist/fetch.js +85 -0
  6. package/dist/fetch.test.d.ts +2 -0
  7. package/dist/fetch.test.d.ts.map +1 -0
  8. package/dist/fetch.test.js +97 -0
  9. package/dist/fluent/kube.d.ts +11 -0
  10. package/dist/fluent/kube.d.ts.map +1 -0
  11. package/dist/fluent/kube.js +100 -0
  12. package/dist/fluent/kube.test.d.ts +2 -0
  13. package/dist/fluent/kube.test.d.ts.map +1 -0
  14. package/dist/fluent/kube.test.js +92 -0
  15. package/dist/fluent/types.d.ts +121 -0
  16. package/dist/fluent/types.d.ts.map +1 -0
  17. package/dist/fluent/types.js +14 -0
  18. package/dist/fluent/utils.d.ts +31 -0
  19. package/dist/fluent/utils.d.ts.map +1 -0
  20. package/dist/fluent/utils.js +116 -0
  21. package/dist/fluent/utils.test.d.ts +2 -0
  22. package/dist/fluent/utils.test.d.ts.map +1 -0
  23. package/dist/fluent/utils.test.js +88 -0
  24. package/dist/fluent/watch.d.ts +8 -0
  25. package/dist/fluent/watch.d.ts.map +1 -0
  26. package/dist/fluent/watch.js +78 -0
  27. package/dist/index.d.ts +7 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +44 -1
  30. package/dist/kinds.d.ts +10 -0
  31. package/dist/kinds.d.ts.map +1 -0
  32. package/dist/kinds.js +489 -0
  33. package/dist/kinds.test.d.ts +2 -0
  34. package/dist/kinds.test.d.ts.map +1 -0
  35. package/dist/kinds.test.js +142 -0
  36. package/dist/types.d.ts +26 -0
  37. package/dist/types.d.ts.map +1 -0
  38. package/dist/types.js +16 -0
  39. package/dist/upstream.d.ts +4 -0
  40. package/dist/upstream.d.ts.map +1 -0
  41. package/dist/upstream.js +54 -0
  42. package/package.json +21 -8
  43. package/src/fetch.test.ts +115 -0
  44. package/src/fetch.ts +72 -0
  45. package/src/fluent/kube.test.ts +136 -0
  46. package/src/fluent/kube.ts +126 -0
  47. package/src/fluent/types.ts +151 -0
  48. package/src/fluent/utils.test.ts +110 -0
  49. package/src/fluent/utils.ts +152 -0
  50. package/src/fluent/watch.ts +94 -0
  51. package/src/index.ts +8 -3
  52. package/src/kinds.test.ts +1 -1
  53. package/src/kinds.ts +1 -1
  54. package/src/types.ts +1 -154
  55. package/.eslintrc.json +0 -26
  56. package/.prettierrc +0 -13
  57. package/coverage/clover.xml +0 -83
  58. package/coverage/coverage-final.json +0 -5
  59. package/coverage/lcov-report/base.css +0 -224
  60. package/coverage/lcov-report/block-navigation.js +0 -87
  61. package/coverage/lcov-report/favicon.png +0 -0
  62. package/coverage/lcov-report/index.html +0 -161
  63. package/coverage/lcov-report/index.ts.html +0 -127
  64. package/coverage/lcov-report/kinds.ts.html +0 -1675
  65. package/coverage/lcov-report/prettify.css +0 -1
  66. package/coverage/lcov-report/prettify.js +0 -2
  67. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  68. package/coverage/lcov-report/sorter.js +0 -196
  69. package/coverage/lcov-report/types.ts.html +0 -643
  70. package/coverage/lcov-report/upstream.ts.html +0 -244
  71. package/coverage/lcov.info +0 -208
  72. package/jest.config.json +0 -4
  73. package/tsconfig.json +0 -18
@@ -0,0 +1,151 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import { KubernetesListObject, KubernetesObject } from "@kubernetes/client-node";
5
+ import { Operation } from "fast-json-patch";
6
+ import { Agent } from "http";
7
+
8
+ import { GenericClass, GroupVersionKind } from "../types";
9
+
10
+ /**
11
+ * The Phase matched when using the K8s Watch API.
12
+ */
13
+ export enum WatchPhase {
14
+ Added = "ADDED",
15
+ Modified = "MODIFIED",
16
+ Deleted = "DELETED",
17
+ }
18
+
19
+ export type FetchMethods = "GET" | "APPLY" | "POST" | "PUT" | "DELETE" | "PATCH" | "WATCH";
20
+
21
+ export interface Filters {
22
+ kindOverride?: GroupVersionKind;
23
+ fields?: Record<string, string>;
24
+ labels?: Record<string, string>;
25
+ name?: string;
26
+ namespace?: string;
27
+ }
28
+
29
+ export type GetFunction<K extends KubernetesObject> = {
30
+ (): Promise<KubernetesListObject<K>>;
31
+ (name: string): Promise<K>;
32
+ };
33
+
34
+ export type KubeFilteredActions<K extends KubernetesObject> = {
35
+ /**
36
+ * Get the resource or resources matching the filters.
37
+ * If no filters are specified, all resources will be returned.
38
+ * If a name is specified, only a single resource will be returned.
39
+ */
40
+ Get: GetFunction<K>;
41
+
42
+ /**
43
+ * Delete the resource if it exists.
44
+ *
45
+ * @param filter - the resource or resource name to delete
46
+ */
47
+ Delete: (filter?: K | string) => Promise<void>;
48
+
49
+ /**
50
+ *
51
+ * @param callback
52
+ * @returns
53
+ */
54
+ Watch: (callback: (payload: K, phase: WatchPhase) => void) => Promise<void>;
55
+ };
56
+
57
+ export type KubeUnfilteredActions<K extends KubernetesObject> = {
58
+ /**
59
+ * Perform a server-side apply of the provided K8s resource.
60
+ *
61
+ * @param resource
62
+ * @returns
63
+ */
64
+ Apply: (resource: K) => Promise<K>;
65
+
66
+ /**
67
+ * Create the provided K8s resource or throw an error if it already exists.
68
+ *
69
+ * @param resource
70
+ * @returns
71
+ */
72
+ Create: (resource: K) => Promise<K>;
73
+
74
+ /**
75
+ * Advanced JSON Patch operations for when Server Side Apply, Kube().Apply(), is insufficient.
76
+ *
77
+ * Note: Throws an error on an empty list of patch operations.
78
+ *
79
+ * @param payload The patch operations to run
80
+ * @returns The patched resource
81
+ */
82
+ Patch: (payload: Operation[]) => Promise<K>;
83
+ };
84
+
85
+ export type KubeWithFilters<K extends KubernetesObject> = KubeFilteredActions<K> & {
86
+ /**
87
+ * Filter the query by the given field.
88
+ * Note multiple calls to this method will result in an AND condition. e.g.
89
+ *
90
+ * ```ts
91
+ * Kube(given.Deployment)
92
+ * .WithField("metadata.name", "bar")
93
+ * .WithField("metadata.namespace", "qux")
94
+ * .Delete(...)
95
+ * ```
96
+ *
97
+ * Will only delete the Deployment if it has the `metadata.name=bar` and `metadata.namespace=qux` fields.
98
+ *
99
+ * @param key The field key
100
+ * @param value The field value
101
+ * @returns
102
+ */
103
+ WithField: <P extends Paths<K>>(key: P, value?: string) => KubeWithFilters<K>;
104
+
105
+ /**
106
+ * Filter the query by the given label. If no value is specified, the label simply must exist.
107
+ * Note multiple calls to this method will result in an AND condition. e.g.
108
+ *
109
+ * ```ts
110
+ * Kube(given.Deployment)
111
+ * .WithLabel("foo", "bar")
112
+ * .WithLabel("baz", "qux")
113
+ * .Delete(...)
114
+ * ```
115
+ *
116
+ * Will only delete the Deployment if it has the`foo=bar` and `baz=qux` labels.
117
+ *
118
+ * @param key The label key
119
+ * @param value (optional) The label value
120
+ */
121
+ WithLabel: (key: string, value?: string) => KubeWithFilters<K>;
122
+ };
123
+
124
+ export type KubeInit<K extends KubernetesObject> = KubeWithFilters<K> &
125
+ KubeUnfilteredActions<K> & {
126
+ /**
127
+ * Filter the query by the given namespace.
128
+ *
129
+ * @param namespace
130
+ * @returns
131
+ */
132
+ InNamespace: (namespace: string) => KubeWithFilters<K>;
133
+ };
134
+
135
+ export type WatchAction<T extends GenericClass, K extends KubernetesObject = InstanceType<T>> = (
136
+ update: K,
137
+ phase: WatchPhase,
138
+ ) => Promise<void> | void;
139
+
140
+ // Special types to handle the recursive keyof typescript lookup
141
+ type Join<K, P> = K extends string | number
142
+ ? P extends string | number
143
+ ? `${K}${"" extends P ? "" : "."}${P}`
144
+ : never
145
+ : never;
146
+
147
+ export type Paths<T, D extends number = 10> = [D] extends [never]
148
+ ? never
149
+ : T extends object
150
+ ? { [K in keyof T]-?: K extends string | number ? `${K}` | Join<K, Paths<T[K]>> : never }[keyof T]
151
+ : "";
@@ -0,0 +1,110 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import { beforeEach, describe, expect, it, jest } from "@jest/globals";
5
+
6
+ import { fetch } from "../fetch";
7
+ import { GenericClass } from "../types";
8
+ import { ClusterRole, Ingress, Pod } from "../upstream";
9
+ import { Filters } from "./types";
10
+ import { kubeExec, pathBuilder } from "./utils";
11
+
12
+ jest.mock("https");
13
+ jest.mock("../fetch");
14
+
15
+ describe("pathBuilder Function", () => {
16
+ const serverUrl = "https://jest-test:8080";
17
+ it("should throw an error if the kind is not specified and the model is not a KubernetesObject", () => {
18
+ const model = { name: "Unknown" } as unknown as GenericClass;
19
+ const filters: Filters = {};
20
+ expect(() => pathBuilder("", model, filters)).toThrow("Kind not specified for Unknown");
21
+ });
22
+
23
+ it("should generate a path for core group kinds", () => {
24
+ const filters: Filters = { namespace: "default", name: "mypod" };
25
+ const result = pathBuilder(serverUrl, Pod, filters);
26
+ const expected = new URL("/api/v1/namespaces/default/pods/mypod", serverUrl);
27
+ expect(result).toEqual(expected);
28
+ });
29
+
30
+ it("should generate a path for non-core group kinds", () => {
31
+ const filters: Filters = {
32
+ namespace: "default",
33
+ name: "myingress",
34
+ };
35
+ const result = pathBuilder(serverUrl, Ingress, filters);
36
+ const expected = new URL(
37
+ "/apis/networking.k8s.io/v1/namespaces/default/ingresses/myingress",
38
+ serverUrl,
39
+ );
40
+ expect(result).toEqual(expected);
41
+ });
42
+
43
+ it("should generate a path without a namespace if not provided", () => {
44
+ const filters: Filters = { name: "tester" };
45
+ const result = pathBuilder(serverUrl, ClusterRole, filters);
46
+ const expected = new URL("/apis/rbac.authorization.k8s.io/v1/clusterroles/tester", serverUrl);
47
+ expect(result).toEqual(expected);
48
+ });
49
+
50
+ it("should generate a path without a name if excludeName is true", () => {
51
+ const filters: Filters = { namespace: "default", name: "mypod" };
52
+ const result = pathBuilder(serverUrl, Pod, filters, true);
53
+ const expected = new URL("/api/v1/namespaces/default/pods", serverUrl);
54
+ expect(result).toEqual(expected);
55
+ });
56
+ });
57
+
58
+ describe("kubeExec Function", () => {
59
+ const mockedFetch = jest.mocked(fetch);
60
+
61
+ const fakeFilters: Filters = { name: "fake", namespace: "default" };
62
+ const fakeMethod = "GET";
63
+ const fakePayload = { metadata: { name: "fake", namespace: "default" } };
64
+ const fakeUrl = new URL("http://jest-test:8080/api/v1/namespaces/default/pods/fake");
65
+ const fakeOpts = {
66
+ body: JSON.stringify(fakePayload),
67
+ headers: {
68
+ "Content-Type": "application/json",
69
+ "User-Agent": `kubernetes-fluent-client`,
70
+ },
71
+ method: fakeMethod,
72
+ };
73
+
74
+ beforeEach(() => {
75
+ mockedFetch.mockClear();
76
+ });
77
+
78
+ it("should make a successful fetch call", async () => {
79
+ mockedFetch.mockResolvedValueOnce({
80
+ ok: true,
81
+ data: fakePayload,
82
+ status: 200,
83
+ statusText: "OK",
84
+ });
85
+
86
+ const result = await kubeExec(Pod, fakeFilters, fakeMethod, fakePayload);
87
+
88
+ expect(result).toEqual(fakePayload);
89
+ expect(mockedFetch).toHaveBeenCalledWith(fakeUrl, expect.objectContaining(fakeOpts));
90
+ });
91
+
92
+ it("should handle fetch call failure", async () => {
93
+ const fakeStatus = 404;
94
+ const fakeStatusText = "Not Found";
95
+
96
+ mockedFetch.mockResolvedValueOnce({
97
+ ok: false,
98
+ data: null,
99
+ status: fakeStatus,
100
+ statusText: fakeStatusText,
101
+ });
102
+
103
+ await expect(kubeExec(Pod, fakeFilters, fakeMethod, fakePayload)).rejects.toEqual(
104
+ expect.objectContaining({
105
+ status: fakeStatus,
106
+ statusText: fakeStatusText,
107
+ }),
108
+ );
109
+ });
110
+ });
@@ -0,0 +1,152 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import { KubeConfig, PatchStrategy } from "@kubernetes/client-node";
5
+
6
+ import { Headers } from "node-fetch";
7
+ import { URL } from "url";
8
+ import { fetch } from "../fetch";
9
+ import { modelToGroupVersionKind } from "../kinds";
10
+ import { GenericClass } from "../types";
11
+ import { FetchMethods, Filters } from "./types";
12
+
13
+ const SSA_CONTENT_TYPE = "application/apply-patch+yaml";
14
+
15
+ /**
16
+ * Generate a path to a Kubernetes resource
17
+ *
18
+ * @param serverUrl
19
+ * @param model
20
+ * @param filters
21
+ * @param excludeName
22
+ * @returns
23
+ */
24
+ export function pathBuilder<T extends GenericClass>(
25
+ serverUrl: string,
26
+ model: T,
27
+ filters: Filters,
28
+ excludeName = false,
29
+ ) {
30
+ const matchedKind = filters.kindOverride || modelToGroupVersionKind(model.name);
31
+
32
+ // If the kind is not specified and the model is not a KubernetesObject, throw an error
33
+ if (!matchedKind) {
34
+ throw new Error(`Kind not specified for ${model.name}`);
35
+ }
36
+
37
+ // Use the plural property if it exists, otherwise use lowercase kind + s
38
+ const plural = matchedKind.plural || `${matchedKind.kind.toLowerCase()}s`;
39
+
40
+ let base = "/api/v1";
41
+
42
+ // If the kind is not in the core group, add the group and version to the path
43
+ if (matchedKind.group) {
44
+ if (!matchedKind.version) {
45
+ throw new Error(`Version not specified for ${model.name}`);
46
+ }
47
+
48
+ base = `/apis/${matchedKind.group}/${matchedKind.version}`;
49
+ }
50
+
51
+ // Namespaced paths require a namespace prefix
52
+ const namespace = filters.namespace ? `namespaces/${filters.namespace}` : "";
53
+
54
+ // Name should not be included in some paths
55
+ const name = excludeName ? "" : filters.name;
56
+
57
+ // Build the complete path to the resource
58
+ const path = [base, namespace, plural, name].filter(Boolean).join("/");
59
+
60
+ // Generate the URL object
61
+ const url = new URL(path, serverUrl);
62
+
63
+ // Add field selectors to the query params
64
+ if (filters.fields) {
65
+ const fieldSelector = Object.entries(filters.fields)
66
+ .map(([key, value]) => `${key}=${value}`)
67
+ .join(",");
68
+
69
+ url.searchParams.set("fieldSelector", fieldSelector);
70
+ }
71
+
72
+ // Add label selectors to the query params
73
+ if (filters.labels) {
74
+ const labelSelector = Object.entries(filters.labels)
75
+ .map(([key, value]) => `${key}=${value}`)
76
+ .join(",");
77
+
78
+ url.searchParams.set("labelSelector", labelSelector);
79
+ }
80
+
81
+ return url;
82
+ }
83
+
84
+ /**
85
+ * Sets up the kubeconfig and https agent for a request
86
+ *
87
+ * A few notes:
88
+ * - The kubeconfig is loaded from the default location, and can check for in-cluster config
89
+ * - We have to create an agent to handle the TLS connection (for the custom CA + mTLS in some cases)
90
+ * - The K8s lib uses request instead of node-fetch today so the object is slightly different
91
+ *
92
+ * @param method
93
+ * @returns
94
+ */
95
+ export async function kubeCfg(method: FetchMethods) {
96
+ const kubeConfig = new KubeConfig();
97
+ kubeConfig.loadFromDefault();
98
+
99
+ const cluster = kubeConfig.getCurrentCluster();
100
+ if (!cluster) {
101
+ throw new Error("No currently active cluster");
102
+ }
103
+
104
+ // Setup the TLS options & auth headers, as needed
105
+ const opts = await kubeConfig.applyToFetchOptions({
106
+ method,
107
+ headers: {
108
+ // Set the default content type to JSON
109
+ "Content-Type": "application/json",
110
+ // Set the user agent like kubectl does
111
+ "User-Agent": `kubernetes-fluent-client`,
112
+ },
113
+ });
114
+
115
+ return { opts, serverUrl: cluster.server };
116
+ }
117
+
118
+ export async function kubeExec<T extends GenericClass, K>(
119
+ model: T,
120
+ filters: Filters,
121
+ method: FetchMethods,
122
+ payload?: K | unknown,
123
+ ) {
124
+ const { opts, serverUrl } = await kubeCfg(method);
125
+ const url = pathBuilder(serverUrl, model, filters, method === "POST");
126
+
127
+ switch (opts.method) {
128
+ case "PATCH":
129
+ (opts.headers as Headers).set("Content-Type", PatchStrategy.JsonPatch);
130
+ break;
131
+
132
+ case "APPLY":
133
+ (opts.headers as Headers).set("Content-Type", SSA_CONTENT_TYPE);
134
+ opts.method = "PATCH";
135
+ url.searchParams.set("fieldManager", "pepr");
136
+ url.searchParams.set("fieldValidation", "Strict");
137
+ url.searchParams.set("force", "false");
138
+ break;
139
+ }
140
+
141
+ if (payload) {
142
+ opts.body = JSON.stringify(payload);
143
+ }
144
+
145
+ const resp = await fetch<K>(url, opts);
146
+
147
+ if (resp.ok) {
148
+ return resp.data;
149
+ }
150
+
151
+ throw resp;
152
+ }
@@ -0,0 +1,94 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import readline from "readline";
5
+
6
+ import fetch from "node-fetch";
7
+ import { GenericClass } from "../types";
8
+ import { Filters, WatchAction, WatchPhase } from "./types";
9
+ import { kubeCfg, pathBuilder } from "./utils";
10
+
11
+ /**
12
+ * Execute a watch on the specified resource.
13
+ */
14
+ export async function ExecWatch<T extends GenericClass>(
15
+ model: T,
16
+ filters: Filters,
17
+ callback: WatchAction<T>,
18
+ ) {
19
+ // Build the path and query params for the resource, excluding the name
20
+ const { opts, serverUrl } = await kubeCfg("GET");
21
+ const url = pathBuilder(serverUrl, model, filters, true);
22
+
23
+ // Enable the watch query param
24
+ url.searchParams.set("watch", "true");
25
+
26
+ // Allow bookmarks to be used for the watch
27
+ url.searchParams.set("allowWatchBookmarks", "true");
28
+
29
+ // If a name is specified, add it to the query params
30
+ if (filters.name) {
31
+ url.searchParams.set("fieldSelector", `metadata.name=${filters.name}`);
32
+ }
33
+
34
+ // Add abort controller to the long-running request
35
+ const controller = new AbortController();
36
+ opts.signal = controller.signal;
37
+
38
+ // Close the connection and make the callback function no-op
39
+ let close = (err?: Error) => {
40
+ controller.abort();
41
+ close = () => {};
42
+ if (err) {
43
+ throw err;
44
+ }
45
+ };
46
+
47
+ try {
48
+ // Make the actual request
49
+ const response = await fetch(url, opts);
50
+
51
+ // If the request is successful, start listening for events
52
+ if (response.ok) {
53
+ const { body } = response;
54
+
55
+ // Bind connection events to the close function
56
+ body.on("error", close);
57
+ body.on("close", close);
58
+ body.on("finish", close);
59
+
60
+ // Create a readline interface to parse the stream
61
+ const rl = readline.createInterface({
62
+ input: response.body!,
63
+ terminal: false,
64
+ });
65
+
66
+ // Listen for events and call the callback function
67
+ rl.on("line", line => {
68
+ try {
69
+ // Parse the event payload
70
+ const { object: payload, type: phase } = JSON.parse(line) as {
71
+ type: WatchPhase;
72
+ object: InstanceType<T>;
73
+ };
74
+
75
+ // Call the callback function with the parsed payload
76
+ void callback(payload, phase as WatchPhase);
77
+ } catch (ignore) {
78
+ // ignore parse errors
79
+ }
80
+ });
81
+ } else {
82
+ // If the request fails, throw an error
83
+ const error = new Error(response.statusText) as Error & {
84
+ statusCode: number | undefined;
85
+ };
86
+ error.statusCode = response.status;
87
+ throw error;
88
+ }
89
+ } catch (e) {
90
+ close(e);
91
+ }
92
+
93
+ return controller;
94
+ }
package/src/index.ts CHANGED
@@ -4,11 +4,16 @@
4
4
  // Export kinds as a single object
5
5
  import * as kind from "./upstream";
6
6
 
7
- /** given is a collection of K8s types to be used within a Kube call: `Kube(Secret).Apply({})`. `a` may also be used in it's place */
7
+ /** given is a collection of K8s types to be used within a Kube call: `Kube(kind.Secret).Apply({})`. `a` may also be used in it's place */
8
8
  export { kind };
9
9
 
10
- // export { Kube } from "./fluent/kube";
10
+ // Export the node-fetch wrapper
11
+ export { fetch } from "./fetch";
11
12
 
12
- export { modelToGroupVersionKind, gvkMap, RegisterKind } from "./kinds";
13
+ // Export the fluent API entrypoint
14
+ export { Kube } from "./fluent/kube";
15
+
16
+ // Export helpers for working with K8s types
17
+ export { modelToGroupVersionKind, RegisterKind } from "./kinds";
13
18
 
14
19
  export * from "./types";
package/src/kinds.test.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
- // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
2
+ // SPDX-FileCopyrightText: 2023-Present The Kubernetes Fluent Client Authors
3
3
 
4
4
  import { expect, test } from "@jest/globals";
5
5
 
package/src/kinds.ts CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  import { GenericClass, GroupVersionKind } from "./types";
5
5
 
6
- export const gvkMap: Record<string, GroupVersionKind> = {
6
+ const gvkMap: Record<string, GroupVersionKind> = {
7
7
  /**
8
8
  * Represents a K8s ClusterRole resource.
9
9
  * ClusterRole is a set of permissions that can be bound to a user or group in a cluster-wide scope.