From 3baa42e17bab0855f88b474be173b0ef7768e4ca Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Tue, 18 Apr 2023 13:47:41 +0300 Subject: [PATCH] New resource view: ValidatingWebhookConfigurations (#7552) Signed-off-by: Alex Andreev --- ...ating-webhook-configurations.injectable.ts | 20 ++ ...webhook-configurations-route.injectable.ts | 24 ++ .../src/common/k8s-api/endpoints/index.ts | 1 + .../mutating-webhook-configuration.api.ts | 5 +- ...ng-webhook-configuration-api.injectable.ts | 27 ++ .../validating-webhook-configuration.api.ts | 47 ++++ packages/core/src/common/rbac.ts | 9 +- .../__snapshots__/details.test.tsx.snap | 232 +++++++++++++++++ .../details.test.tsx | 82 ++++++ ...utating-webhook-configurations-details.tsx | 151 +---------- ...s.module.css => webhook-config.module.css} | 0 .../webhook-config.tsx | 166 +++++++++++++ .../__snapshots__/details.test.tsx.snap | 234 ++++++++++++++++++ .../details.test.tsx | 83 +++++++ .../index.ts | 6 + ...-webhook-configuration-store.injectable.ts | 26 ++ .../validating-webhook-configuration-store.ts | 22 ++ ...idating-webhook-configurations-details.tsx | 35 +++ ...nfigurations-route-component.injectable.ts | 24 ++ ...configurations-sidebar-items.injectable.ts | 39 +++ .../validating-webhook-configurations.tsx | 73 ++++++ ...-configurations-details-item.injectable.ts | 33 +++ packages/core/src/renderer/utils/rbac.ts | 1 + .../__tests__/cluster-pages.tests.ts | 12 + 24 files changed, 1200 insertions(+), 152 deletions(-) create mode 100644 packages/core/src/common/front-end-routing/routes/cluster/config/validating-webhook-configurations/navigate-to-validating-webhook-configurations.injectable.ts create mode 100644 packages/core/src/common/front-end-routing/routes/cluster/config/validating-webhook-configurations/validating-webhook-configurations-route.injectable.ts create mode 100644 packages/core/src/common/k8s-api/endpoints/validating-webhook-configuration-api.injectable.ts create mode 100644 packages/core/src/common/k8s-api/endpoints/validating-webhook-configuration.api.ts create mode 100644 packages/core/src/renderer/components/+config-mutating-webhook-configurations/__snapshots__/details.test.tsx.snap create mode 100644 packages/core/src/renderer/components/+config-mutating-webhook-configurations/details.test.tsx rename packages/core/src/renderer/components/+config-mutating-webhook-configurations/{mutating-webhook-configs-details.module.css => webhook-config.module.css} (100%) create mode 100644 packages/core/src/renderer/components/+config-mutating-webhook-configurations/webhook-config.tsx create mode 100644 packages/core/src/renderer/components/+config-validating-webhook-configurations/__snapshots__/details.test.tsx.snap create mode 100644 packages/core/src/renderer/components/+config-validating-webhook-configurations/details.test.tsx create mode 100644 packages/core/src/renderer/components/+config-validating-webhook-configurations/index.ts create mode 100644 packages/core/src/renderer/components/+config-validating-webhook-configurations/validating-webhook-configuration-store.injectable.ts create mode 100644 packages/core/src/renderer/components/+config-validating-webhook-configurations/validating-webhook-configuration-store.ts create mode 100644 packages/core/src/renderer/components/+config-validating-webhook-configurations/validating-webhook-configurations-details.tsx create mode 100644 packages/core/src/renderer/components/+config-validating-webhook-configurations/validating-webhook-configurations-route-component.injectable.ts create mode 100644 packages/core/src/renderer/components/+config-validating-webhook-configurations/validating-webhook-configurations-sidebar-items.injectable.ts create mode 100644 packages/core/src/renderer/components/+config-validating-webhook-configurations/validating-webhook-configurations.tsx create mode 100644 packages/core/src/renderer/components/kube-object-details/kube-object-detail-items/implementations/validating-webhook-configurations-details-item.injectable.ts diff --git a/packages/core/src/common/front-end-routing/routes/cluster/config/validating-webhook-configurations/navigate-to-validating-webhook-configurations.injectable.ts b/packages/core/src/common/front-end-routing/routes/cluster/config/validating-webhook-configurations/navigate-to-validating-webhook-configurations.injectable.ts new file mode 100644 index 0000000000..47e4535765 --- /dev/null +++ b/packages/core/src/common/front-end-routing/routes/cluster/config/validating-webhook-configurations/navigate-to-validating-webhook-configurations.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import validatingWebhookConfigurationsRouteInjectable from "./validating-webhook-configurations-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token"; + +const navigateToValidatingWebhookConfigurationsInjectable = getInjectable({ + id: "navigate-to-validating-webhook-configurations", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(validatingWebhookConfigurationsRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToValidatingWebhookConfigurationsInjectable; diff --git a/packages/core/src/common/front-end-routing/routes/cluster/config/validating-webhook-configurations/validating-webhook-configurations-route.injectable.ts b/packages/core/src/common/front-end-routing/routes/cluster/config/validating-webhook-configurations/validating-webhook-configurations-route.injectable.ts new file mode 100644 index 0000000000..6307238350 --- /dev/null +++ b/packages/core/src/common/front-end-routing/routes/cluster/config/validating-webhook-configurations/validating-webhook-configurations-route.injectable.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { shouldShowResourceInjectionToken } from "../../../../../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token"; +import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token"; +import { getInjectable } from "@ogre-tools/injectable"; + +const validatingWebhookConfigurationsRouteInjectable = getInjectable({ + id: "validatingwebhookconfigurations", + + instantiate: (di) => ({ + path: "/validatingwebhookconfigurations", + clusterFrame: true, + isEnabled: di.inject(shouldShowResourceInjectionToken, { + apiName: "validatingwebhookconfigurations", + group: "admissionregistration.k8s.io", + }), + }), + + injectionToken: frontEndRouteInjectionToken, +}); + +export default validatingWebhookConfigurationsRouteInjectable; diff --git a/packages/core/src/common/k8s-api/endpoints/index.ts b/packages/core/src/common/k8s-api/endpoints/index.ts index 5a83bef417..ad187f6565 100644 --- a/packages/core/src/common/k8s-api/endpoints/index.ts +++ b/packages/core/src/common/k8s-api/endpoints/index.ts @@ -23,6 +23,7 @@ export * from "./job.api"; export * from "./lease.api"; export * from "./limit-range.api"; export * from "./mutating-webhook-configuration.api"; +export * from "./validating-webhook-configuration.api"; export * from "./namespace.api"; export * from "./network-policy.api"; export * from "./node.api"; diff --git a/packages/core/src/common/k8s-api/endpoints/mutating-webhook-configuration.api.ts b/packages/core/src/common/k8s-api/endpoints/mutating-webhook-configuration.api.ts index bdfde072bf..241a5c09cb 100644 --- a/packages/core/src/common/k8s-api/endpoints/mutating-webhook-configuration.api.ts +++ b/packages/core/src/common/k8s-api/endpoints/mutating-webhook-configuration.api.ts @@ -43,7 +43,7 @@ interface RuleWithOperations { scope?: string; } -interface MutatingWebhook { +export interface Webhook { // The name of the webhook configuration. name: string; @@ -94,6 +94,9 @@ interface MutatingWebhook { reinvocationPolicy?: "Never" | "IfNeeded"; } +export interface MutatingWebhook extends Webhook { +} + interface ServiceReference { // `namespace` is the namespace of the service. namespace: string; diff --git a/packages/core/src/common/k8s-api/endpoints/validating-webhook-configuration-api.injectable.ts b/packages/core/src/common/k8s-api/endpoints/validating-webhook-configuration-api.injectable.ts new file mode 100644 index 0000000000..a1d6e40417 --- /dev/null +++ b/packages/core/src/common/k8s-api/endpoints/validating-webhook-configuration-api.injectable.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import assert from "assert"; +import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token"; +import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token"; +import loggerInjectable from "../../logger.injectable"; +import maybeKubeApiInjectable from "../maybe-kube-api.injectable"; +import { ValidatingWebhookConfigurationApi } from "./validating-webhook-configuration.api"; + +const validatingWebhookConfigurationApiInjectable = getInjectable({ + id: "validating-webhook-configuration", + instantiate: (di) => { + assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "validatingWebhookApi is only available in certain environments"); + + return new ValidatingWebhookConfigurationApi({ + logger: di.inject(loggerInjectable), + maybeKubeApi: di.inject(maybeKubeApiInjectable), + }); + }, + + injectionToken: kubeApiInjectionToken, +}); + +export default validatingWebhookConfigurationApiInjectable; diff --git a/packages/core/src/common/k8s-api/endpoints/validating-webhook-configuration.api.ts b/packages/core/src/common/k8s-api/endpoints/validating-webhook-configuration.api.ts new file mode 100644 index 0000000000..bb0f9d5117 --- /dev/null +++ b/packages/core/src/common/k8s-api/endpoints/validating-webhook-configuration.api.ts @@ -0,0 +1,47 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { NamespaceScopedMetadata, KubeObjectMetadata, KubeObjectScope } from "../kube-object"; +import { KubeObject } from "../kube-object"; +import type { DerivedKubeApiOptions, KubeApiDependencies } from "../kube-api"; +import { KubeApi } from "../kube-api"; +import type { KubeJsonApiData } from "../kube-json-api"; +import type { Webhook } from "./mutating-webhook-configuration.api"; + +export interface ValidatingWebhook extends Webhook { +} + +interface ValidatingWebhookConfigurationData extends KubeJsonApiData, void, void> { + webhooks?: ValidatingWebhook[]; +} + +export class ValidatingWebhookConfiguration extends KubeObject< + NamespaceScopedMetadata, + void, + void +> { + static kind = "ValidatingWebhookConfiguration"; + static namespaced = false; + static apiBase = "/apis/admissionregistration.k8s.io/v1/validatingwebhookconfigurations"; + + webhooks?: ValidatingWebhook[]; + + constructor({ webhooks, ...rest }: ValidatingWebhookConfigurationData) { + super(rest); + this.webhooks = webhooks; + } + + getWebhooks(): ValidatingWebhook[] { + return this.webhooks ?? []; + } +} + +export class ValidatingWebhookConfigurationApi extends KubeApi { + constructor(deps: KubeApiDependencies, opts?: DerivedKubeApiOptions) { + super(deps, { + ...opts ?? {}, + objectConstructor: ValidatingWebhookConfiguration, + }); + } +} diff --git a/packages/core/src/common/rbac.ts b/packages/core/src/common/rbac.ts index b1a7ceb550..f42b07e775 100644 --- a/packages/core/src/common/rbac.ts +++ b/packages/core/src/common/rbac.ts @@ -9,7 +9,7 @@ export type KubeResource = "pods" | "daemonsets" | "deployments" | "statefulsets" | "replicasets" | "replicationcontrollers" | "jobs" | "cronjobs" | "endpoints" | "customresourcedefinitions" | "horizontalpodautoscalers" | "verticalpodautoscalers" | "podsecuritypolicies" | "poddisruptionbudgets" | "priorityclasses" | "runtimeclasses" | - "roles" | "clusterroles" | "rolebindings" | "clusterrolebindings" | "serviceaccounts" | "mutatingwebhookconfigurations"; + "roles" | "clusterroles" | "rolebindings" | "clusterrolebindings" | "serviceaccounts" | "mutatingwebhookconfigurations" | "validatingwebhookconfigurations"; export interface KubeApiResource { kind: string; @@ -119,7 +119,12 @@ export const apiResourceRecord: Record = { mutatingwebhookconfigurations: { kind: "MutatingWebhookConfiguration", group: "admissionregistration.k8s.io/v1", - namespaced: true, + namespaced: false, + }, + validatingwebhookconfigurations: { + kind: "ValidatingWebhookConfiguration", + group: "admissionregistration.k8s.io/v1", + namespaced: false, }, networkpolicies: { kind: "NetworkPolicy", diff --git a/packages/core/src/renderer/components/+config-mutating-webhook-configurations/__snapshots__/details.test.tsx.snap b/packages/core/src/renderer/components/+config-mutating-webhook-configurations/__snapshots__/details.test.tsx.snap new file mode 100644 index 0000000000..923c494216 --- /dev/null +++ b/packages/core/src/renderer/components/+config-mutating-webhook-configurations/__snapshots__/details.test.tsx.snap @@ -0,0 +1,232 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MutatingWebhookConfigsDetails renders 1`] = ` + +
+
+
+ + API version + + + admissionregistration.k8s.io/v1 + +
+
+ Webhooks +
+
+ + Name + + + + pod-policy.example.com + + +
+
+ + Client Config + + +
+
+ Name: + + service-name +
+
+ Namespace: + + service-namespace +
+
+
+
+
+ + Match Policy + + +
+
+ + Failure Policy + + + Fail + +
+
+ + Admission Review Versions + + + v1, v1beta1 + +
+
+ + Side Effects + + + None + +
+
+ + Timeout Seconds + + +
+
+ + Namespace Selector + + +
+
+ + Object Selector + + +
+
+ + Rules + + +
+
+ API Groups: + + , apps, extensions +
+
+ API Versions: + + v1, v1beta1 +
+
+ Operations: + + CREATE, UPDATE +
+
+ Resources: + + pods, deployments +
+
+
+
+
+
+ +`; + +exports[`MutatingWebhookConfigsDetails renders with no webhooks 1`] = ` + +
+
+
+ + API version + + + admissionregistration.k8s.io/v1 + +
+
+ Webhooks +
+
+ No webhooks set +
+
+
+ +`; diff --git a/packages/core/src/renderer/components/+config-mutating-webhook-configurations/details.test.tsx b/packages/core/src/renderer/components/+config-mutating-webhook-configurations/details.test.tsx new file mode 100644 index 0000000000..c02b3047a7 --- /dev/null +++ b/packages/core/src/renderer/components/+config-mutating-webhook-configurations/details.test.tsx @@ -0,0 +1,82 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { RenderResult } from "@testing-library/react"; +import React from "react"; +import { + MutatingWebhookConfiguration, +} from "../../../common/k8s-api/endpoints"; +import { getDiForUnitTesting } from "../../getDiForUnitTesting"; +import type { DiRender } from "../test-utils/renderFor"; +import { renderFor } from "../test-utils/renderFor"; +import { MutatingWebhookDetails } from "./mutating-webhook-configurations-details"; + +const mutatingWebhookConfig = { + apiVersion: "admissionregistration.k8s.io/v1", + kind: "MutatingWebhookConfiguration", + metadata: { + name: "pod-policy.example.com", + resourceVersion: "1", + uid: "pod-policy.example.com", + namespace: "default", + selfLink: "/apis/admissionregistration.k8s.io/v1/pod-policy.example.com", + }, + webhooks: [ + { + name: "pod-policy.example.com", + rules: [ + { + apiGroups: ["", "apps", "extensions"], + apiVersions: ["v1", "v1beta1"], + operations: ["CREATE", "UPDATE"], + resources: ["pods", "deployments"], + }, + ], + failurePolicy: "Fail", + admissionReviewVersions: ["v1", "v1beta1"], + sideEffects: "None", + clientConfig: { + service: { + namespace: "service-namespace", + name: "service-name", + }, + caBundle: "Cg==", + }, + }, + ], +}; + +describe("MutatingWebhookConfigsDetails", () => { + let result: RenderResult; + let render: DiRender; + + beforeEach(async () => { + const di = getDiForUnitTesting(); + + render = renderFor(di); + }); + + it("renders", () => { + const webhookConfig = new MutatingWebhookConfiguration(mutatingWebhookConfig); + + result = render( + , + ); + + expect(result.baseElement).toMatchSnapshot(); + }); + + it("renders with no webhooks", () => { + const webhookConfig = new MutatingWebhookConfiguration({ + ...mutatingWebhookConfig, + webhooks: [], + }); + + result = render( + , + ); + + expect(result.baseElement).toMatchSnapshot(); + }); +}); diff --git a/packages/core/src/renderer/components/+config-mutating-webhook-configurations/mutating-webhook-configurations-details.tsx b/packages/core/src/renderer/components/+config-mutating-webhook-configurations/mutating-webhook-configurations-details.tsx index d40a00ad81..594ee3c43e 100644 --- a/packages/core/src/renderer/components/+config-mutating-webhook-configurations/mutating-webhook-configurations-details.tsx +++ b/packages/core/src/renderer/components/+config-mutating-webhook-configurations/mutating-webhook-configurations-details.tsx @@ -2,14 +2,12 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import styles from "./mutating-webhook-configs-details.module.css"; - import React from "react"; import { observer } from "mobx-react"; import { DrawerItem, DrawerTitle } from "../drawer"; import type { KubeObjectDetailsProps } from "../kube-object-details"; import type { MutatingWebhookConfiguration } from "../../../common/k8s-api/endpoints"; -import { Badge } from "../badge"; +import { WebhookConfig } from "./webhook-config"; export interface MutatingWebhookDetailsProps extends KubeObjectDetailsProps { } @@ -29,152 +27,7 @@ export class MutatingWebhookDetails extends React.ComponentNo webhooks set )} {webhookConfig.getWebhooks()?.map((webhook) => ( - - - {webhook.name} - - - {webhook.clientConfig?.service?.name && ( -
-
- Name: - {" "} - {webhook.clientConfig.service.name} -
-
- Namespace: - {" "} - {webhook.clientConfig.service.namespace} -
-
- )} -
- - {webhook.matchPolicy} - - - {webhook.failurePolicy} - - - {webhook.admissionReviewVersions?.join(", ")} - - - {webhook.reinvocationPolicy} - - - {webhook.sideEffects} - - - {webhook.timeoutSeconds} - - - {webhook.namespaceSelector && ( -
-
Match Expressions:
- {webhook.namespaceSelector.matchExpressions?.map((expression, index) => ( -
-
- Key: - {" "} - {expression.key} -
-
- Operator: - {" "} - {expression.operator} -
-
- Values: - {" "} - {expression.values?.join(", ")} -
-
- ))} - {webhook.namespaceSelector.matchLabels && ( -
-
Match Labels:
-
- {Object.entries(webhook.namespaceSelector.matchLabels).map(([key, value], index) => ( - - ))} -
-
- )} -
- )} -
- - {webhook.objectSelector && ( -
-
Match Expressions:
- {webhook.objectSelector.matchExpressions?.map((expression, index) => ( -
-
- Key: - {" "} - {expression.key} -
-
- Operator: - {" "} - {expression.operator} -
-
- Values: - {" "} - {expression.values?.join(", ")} -
-
- ))} - {webhook.objectSelector.matchLabels && ( -
-
Match Labels:
-
- {Object.entries(webhook.objectSelector.matchLabels).map(([key, value], index) => ( - - ))} -
-
- )} -
- )} -
- - {webhook.rules?.map((rule, index) => ( -
-
- API Groups: - {" "} - {rule.apiGroups.join(", ")} -
-
- API Versions: - {" "} - {rule.apiVersions?.join(", ")} -
-
- Operations: - {" "} - {rule.operations.join(", ")} -
- {rule.resources && ( -
- Resources: - {" "} - {rule.resources.join(", ")} -
- )} - {rule.scope && ( -
- Scope: - {" "} - {rule.scope} -
- )} -
- ))} -
-
+ ))} ); diff --git a/packages/core/src/renderer/components/+config-mutating-webhook-configurations/mutating-webhook-configs-details.module.css b/packages/core/src/renderer/components/+config-mutating-webhook-configurations/webhook-config.module.css similarity index 100% rename from packages/core/src/renderer/components/+config-mutating-webhook-configurations/mutating-webhook-configs-details.module.css rename to packages/core/src/renderer/components/+config-mutating-webhook-configurations/webhook-config.module.css diff --git a/packages/core/src/renderer/components/+config-mutating-webhook-configurations/webhook-config.tsx b/packages/core/src/renderer/components/+config-mutating-webhook-configurations/webhook-config.tsx new file mode 100644 index 0000000000..f33d50c6ec --- /dev/null +++ b/packages/core/src/renderer/components/+config-mutating-webhook-configurations/webhook-config.tsx @@ -0,0 +1,166 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import React from "react"; +import styles from "./webhook-config.module.css"; +import type { MutatingWebhook, ValidatingWebhook } from "../../../common/k8s-api/endpoints"; +import { DrawerItem } from "../drawer"; +import { Badge } from "../badge"; + +interface WebhookProps { + webhook: ValidatingWebhook | MutatingWebhook; +} + +export const WebhookConfig: React.FC = ({ webhook }) => { + return ( + <> + + {webhook.name} + + + {webhook.clientConfig?.service?.name && ( +
+
+ Name: + {" "} + {webhook.clientConfig.service.name} +
+
+ Namespace: + {" "} + {webhook.clientConfig.service.namespace} +
+
+ )} +
+ + {webhook.matchPolicy} + + + {webhook.failurePolicy} + + + {webhook.admissionReviewVersions?.join(", ")} + + { webhook.reinvocationPolicy && ( + + {webhook.reinvocationPolicy} + + )} + + {webhook.sideEffects} + + + {webhook.timeoutSeconds} + + + {webhook.namespaceSelector && ( +
+
Match Expressions:
+ {webhook.namespaceSelector.matchExpressions?.map((expression, index) => ( +
+
+ Key: + {" "} + {expression.key} +
+
+ Operator: + {" "} + {expression.operator} +
+
+ Values: + {" "} + {expression.values?.join(", ")} +
+
+ ))} + {webhook.namespaceSelector.matchLabels && ( +
+
Match Labels:
+
+ {Object.entries(webhook.namespaceSelector.matchLabels).map(([key, value], index) => ( + + ))} +
+
+ )} +
+ )} +
+ + {webhook.objectSelector && ( +
+
Match Expressions:
+ {webhook.objectSelector.matchExpressions?.map((expression, index) => ( +
+
+ Key: + {" "} + {expression.key} +
+
+ Operator: + {" "} + {expression.operator} +
+
+ Values: + {" "} + {expression.values?.join(", ")} +
+
+ ))} + {webhook.objectSelector.matchLabels && ( +
+
Match Labels:
+
+ {Object.entries(webhook.objectSelector.matchLabels).map(([key, value], index) => ( + + ))} +
+
+ )} +
+ )} +
+ + {webhook.rules?.map((rule, index) => ( +
+
+ API Groups: + {" "} + {rule.apiGroups.join(", ")} +
+
+ API Versions: + {" "} + {rule.apiVersions?.join(", ")} +
+
+ Operations: + {" "} + {rule.operations.join(", ")} +
+ {rule.resources && ( +
+ Resources: + {" "} + {rule.resources.join(", ")} +
+ )} + {rule.scope && ( +
+ Scope: + {" "} + {rule.scope} +
+ )} +
+ ))} +
+ + ); +}; diff --git a/packages/core/src/renderer/components/+config-validating-webhook-configurations/__snapshots__/details.test.tsx.snap b/packages/core/src/renderer/components/+config-validating-webhook-configurations/__snapshots__/details.test.tsx.snap new file mode 100644 index 0000000000..0970626818 --- /dev/null +++ b/packages/core/src/renderer/components/+config-validating-webhook-configurations/__snapshots__/details.test.tsx.snap @@ -0,0 +1,234 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ValidatingWebhookConfigsDetails renders 1`] = ` + +
+
+
+ + API version + + + admissionregistration.k8s.io/v1 + +
+
+ Webhooks +
+
+ + Name + + + + pod-policy.example.com + + +
+
+ + Client Config + + +
+
+ Name: + + service-name +
+
+ Namespace: + + service-namespace +
+
+
+
+
+ + Match Policy + + +
+
+ + Failure Policy + + + Fail + +
+
+ + Admission Review Versions + + + v1, v1beta1 + +
+
+ + Side Effects + + + None + +
+
+ + Timeout Seconds + + + 5 + +
+
+ + Namespace Selector + + +
+
+ + Object Selector + + +
+
+ + Rules + + +
+
+ API Groups: + + , apps, extensions +
+
+ API Versions: + + v1, v1beta1 +
+
+ Operations: + + CREATE, UPDATE +
+
+ Resources: + + pods, deployments +
+
+
+
+
+
+ +`; + +exports[`ValidatingWebhookConfigsDetails renders with no webhooks 1`] = ` + +
+
+
+ + API version + + + admissionregistration.k8s.io/v1 + +
+
+ Webhooks +
+
+ No webhooks set +
+
+
+ +`; diff --git a/packages/core/src/renderer/components/+config-validating-webhook-configurations/details.test.tsx b/packages/core/src/renderer/components/+config-validating-webhook-configurations/details.test.tsx new file mode 100644 index 0000000000..3ccb7e0725 --- /dev/null +++ b/packages/core/src/renderer/components/+config-validating-webhook-configurations/details.test.tsx @@ -0,0 +1,83 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { RenderResult } from "@testing-library/react"; +import React from "react"; +import { + ValidatingWebhookConfiguration, +} from "../../../common/k8s-api/endpoints"; +import { getDiForUnitTesting } from "../../getDiForUnitTesting"; +import type { DiRender } from "../test-utils/renderFor"; +import { renderFor } from "../test-utils/renderFor"; +import { ValidatingWebhookDetails } from "./validating-webhook-configurations-details"; + +const validatingWebhookConfig = { + apiVersion: "admissionregistration.k8s.io/v1", + kind: "ValidatingWebhookConfiguration", + metadata: { + name: "pod-policy.example.com", + resourceVersion: "1", + uid: "pod-policy.example.com", + namespace: "default", + selfLink: "/apis/admissionregistration.k8s.io/v1/pod-policy.example.com", + }, + webhooks: [ + { + name: "pod-policy.example.com", + rules: [ + { + apiGroups: ["", "apps", "extensions"], + apiVersions: ["v1", "v1beta1"], + operations: ["CREATE", "UPDATE"], + resources: ["pods", "deployments"], + }, + ], + failurePolicy: "Fail", + admissionReviewVersions: ["v1", "v1beta1"], + sideEffects: "None", + timeoutSeconds: 5, + clientConfig: { + service: { + namespace: "service-namespace", + name: "service-name", + }, + caBundle: "Cg==", + }, + }, + ], +}; + +describe("ValidatingWebhookConfigsDetails", () => { + let result: RenderResult; + let render: DiRender; + + beforeEach(async () => { + const di = getDiForUnitTesting(); + + render = renderFor(di); + }); + + it("renders", () => { + const webhookConfig = new ValidatingWebhookConfiguration(validatingWebhookConfig); + + result = render( + , + ); + + expect(result.baseElement).toMatchSnapshot(); + }); + + it("renders with no webhooks", () => { + const webhookConfig = new ValidatingWebhookConfiguration({ + ...validatingWebhookConfig, + webhooks: [], + }); + + result = render( + , + ); + + expect(result.baseElement).toMatchSnapshot(); + }); +}); diff --git a/packages/core/src/renderer/components/+config-validating-webhook-configurations/index.ts b/packages/core/src/renderer/components/+config-validating-webhook-configurations/index.ts new file mode 100644 index 0000000000..c584ef3864 --- /dev/null +++ b/packages/core/src/renderer/components/+config-validating-webhook-configurations/index.ts @@ -0,0 +1,6 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +export * from "./validating-webhook-configurations"; +export * from "./validating-webhook-configurations-details"; diff --git a/packages/core/src/renderer/components/+config-validating-webhook-configurations/validating-webhook-configuration-store.injectable.ts b/packages/core/src/renderer/components/+config-validating-webhook-configurations/validating-webhook-configuration-store.injectable.ts new file mode 100644 index 0000000000..44df67ebdb --- /dev/null +++ b/packages/core/src/renderer/components/+config-validating-webhook-configurations/validating-webhook-configuration-store.injectable.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { kubeObjectStoreInjectionToken } from "../../../common/k8s-api/api-manager/kube-object-store-token"; +import { ValidatingWebhookConfigurationStore } from "./validating-webhook-configuration-store"; +import clusterFrameContextForNamespacedResourcesInjectable from "../../cluster-frame-context/for-namespaced-resources.injectable"; +import loggerInjectable from "../../../common/logger.injectable"; +import validatingWebhookConfigurationApiInjectable + from "../../../common/k8s-api/endpoints/validating-webhook-configuration-api.injectable"; + +const validatingWebhookConfigurationStoreInjectable = getInjectable({ + id: "validating-webhook-configuration-store", + instantiate: (di) => { + const api = di.inject(validatingWebhookConfigurationApiInjectable); + + return new ValidatingWebhookConfigurationStore({ + context: di.inject(clusterFrameContextForNamespacedResourcesInjectable), + logger: di.inject(loggerInjectable), + }, api); + }, + injectionToken: kubeObjectStoreInjectionToken, +}); + +export default validatingWebhookConfigurationStoreInjectable; diff --git a/packages/core/src/renderer/components/+config-validating-webhook-configurations/validating-webhook-configuration-store.ts b/packages/core/src/renderer/components/+config-validating-webhook-configurations/validating-webhook-configuration-store.ts new file mode 100644 index 0000000000..b3e6eb2d66 --- /dev/null +++ b/packages/core/src/renderer/components/+config-validating-webhook-configurations/validating-webhook-configuration-store.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { + ValidatingWebhookConfiguration, + ValidatingWebhookConfigurationApi, +} from "../../../common/k8s-api/endpoints"; +import type { + KubeObjectStoreDependencies, + KubeObjectStoreOptions, +} from "../../../common/k8s-api/kube-object.store"; +import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; + +export interface ValidatingWebhookConfigurationStoreDependencies extends KubeObjectStoreDependencies { +} + +export class ValidatingWebhookConfigurationStore extends KubeObjectStore { + constructor(protected readonly dependencies: ValidatingWebhookConfigurationStoreDependencies, api: ValidatingWebhookConfigurationApi, opts?: KubeObjectStoreOptions) { + super(dependencies, api, opts); + } +} diff --git a/packages/core/src/renderer/components/+config-validating-webhook-configurations/validating-webhook-configurations-details.tsx b/packages/core/src/renderer/components/+config-validating-webhook-configurations/validating-webhook-configurations-details.tsx new file mode 100644 index 0000000000..25b74da2a3 --- /dev/null +++ b/packages/core/src/renderer/components/+config-validating-webhook-configurations/validating-webhook-configurations-details.tsx @@ -0,0 +1,35 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import React from "react"; +import { observer } from "mobx-react"; +import { DrawerItem, DrawerTitle } from "../drawer"; +import type { KubeObjectDetailsProps } from "../kube-object-details"; +import { WebhookConfig } from "../+config-mutating-webhook-configurations/webhook-config"; +import type { ValidatingWebhookConfiguration } from "../../../common/k8s-api/endpoints"; + +export interface ValidatingWebhookProps extends KubeObjectDetailsProps { +} + +@observer +export class ValidatingWebhookDetails extends React.Component { + render() { + const { object: webhookConfig } = this.props; + + return ( +
+ + {webhookConfig.apiVersion} + + Webhooks + {webhookConfig.getWebhooks()?.length == 0 && ( +
No webhooks set
+ )} + {webhookConfig.getWebhooks()?.map((webhook) => ( + + ))} +
+ ); + } +} diff --git a/packages/core/src/renderer/components/+config-validating-webhook-configurations/validating-webhook-configurations-route-component.injectable.ts b/packages/core/src/renderer/components/+config-validating-webhook-configurations/validating-webhook-configurations-route-component.injectable.ts new file mode 100644 index 0000000000..564c9b3c48 --- /dev/null +++ b/packages/core/src/renderer/components/+config-validating-webhook-configurations/validating-webhook-configurations-route-component.injectable.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { + routeSpecificComponentInjectionToken, +} from "../../routes/route-specific-component-injection-token"; +import validatingWebhookConfigurationsRouteInjectable + from "../../../common/front-end-routing/routes/cluster/config/validating-webhook-configurations/validating-webhook-configurations-route.injectable"; +import { ValidatingWebhookConfigurations } from "./validating-webhook-configurations"; + +const validatingWebhookConfigurationsRouteComponentInjectable = getInjectable({ + id: "validating-webhook-configuration-route-component", + + instantiate: (di) => ({ + route: di.inject(validatingWebhookConfigurationsRouteInjectable), + Component: ValidatingWebhookConfigurations, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default validatingWebhookConfigurationsRouteComponentInjectable; diff --git a/packages/core/src/renderer/components/+config-validating-webhook-configurations/validating-webhook-configurations-sidebar-items.injectable.ts b/packages/core/src/renderer/components/+config-validating-webhook-configurations/validating-webhook-configurations-sidebar-items.injectable.ts new file mode 100644 index 0000000000..959fb75fe7 --- /dev/null +++ b/packages/core/src/renderer/components/+config-validating-webhook-configurations/validating-webhook-configurations-sidebar-items.injectable.ts @@ -0,0 +1,39 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import { sidebarItemsInjectionToken } from "../layout/sidebar-items.injectable"; +import routeIsActiveInjectable from "../../routes/route-is-active.injectable"; +import validatingWebhookConfigurationsRouteInjectable + from "../../../common/front-end-routing/routes/cluster/config/validating-webhook-configurations/validating-webhook-configurations-route.injectable"; +import navigateToValidatingWebhookConfigurationsInjectable + from "../../../common/front-end-routing/routes/cluster/config/validating-webhook-configurations/navigate-to-validating-webhook-configurations.injectable"; +import { configSidebarItemId } from "../+config/config-sidebar-items.injectable"; + +const validatingWebhookConfigurationsSidebarItemsInjectable = getInjectable({ + id: "validating-webhook-configurations-sidebar-items", + + instantiate: (di) => { + const route = di.inject(validatingWebhookConfigurationsRouteInjectable); + const navigateToPage = di.inject(navigateToValidatingWebhookConfigurationsInjectable); + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return computed(() => [ + { + id: "validating-webhook-configurations", + parentId: configSidebarItemId, + title: "Validating Webhook Configs", + onClick: navigateToPage, + isActive: routeIsActive, + isVisible: route.isEnabled, + orderNumber: 100, + }, + ]); + }, + + injectionToken: sidebarItemsInjectionToken, +}); + +export default validatingWebhookConfigurationsSidebarItemsInjectable; diff --git a/packages/core/src/renderer/components/+config-validating-webhook-configurations/validating-webhook-configurations.tsx b/packages/core/src/renderer/components/+config-validating-webhook-configurations/validating-webhook-configurations.tsx new file mode 100644 index 0000000000..f3427f99c3 --- /dev/null +++ b/packages/core/src/renderer/components/+config-validating-webhook-configurations/validating-webhook-configurations.tsx @@ -0,0 +1,73 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import React from "react"; +import { observer } from "mobx-react"; +import { KubeObjectListLayout } from "../kube-object-list-layout"; +import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout"; +import type { ValidatingWebhookConfigurationStore } from "./validating-webhook-configuration-store"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import validatingWebhookConfigurationsStoreInjectable from "./validating-webhook-configuration-store.injectable"; +import { KubeObjectAge } from "../kube-object/age"; + +enum columnId { + name = "name", + webhooks = "webhooks", + age = "age", +} + +interface Dependencies { + store: ValidatingWebhookConfigurationStore; +} + +const NonInjectedValidatingWebhookConfigurations = observer((props: Dependencies) => { + return ( + + ({ + ...rest, + searchProps: { + ...searchProps, + placeholder: "Search...", + }, + })} + tableId="config_validating_webhook_configurations" + className={"ValidatingWebhookConfigurations"} + store={props.store} + sortingCallbacks={{ + [columnId.name]: item => item.getName(), + [columnId.webhooks]: item => item.getWebhooks().length, + [columnId.age]: item => -item.getCreationTimestamp(), + }} + searchFilters={[ + item => item.getSearchFields(), + item => item.getLabels(), + ]} + renderHeaderTitle="Validating Webhook Configs" + renderTableHeader={[ + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { + title: "Webhooks", + sortBy: columnId.webhooks, + id: columnId.webhooks, + }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + ]} + renderTableContents={item => [ + item.getName(), + item.getWebhooks().length, + , + ]} + /> + + ); +}); + +export const ValidatingWebhookConfigurations = withInjectables(NonInjectedValidatingWebhookConfigurations, { + getProps: (di, props) => ({ + ...props, + store: di.inject(validatingWebhookConfigurationsStoreInjectable), + }), +}); diff --git a/packages/core/src/renderer/components/kube-object-details/kube-object-detail-items/implementations/validating-webhook-configurations-details-item.injectable.ts b/packages/core/src/renderer/components/kube-object-details/kube-object-detail-items/implementations/validating-webhook-configurations-details-item.injectable.ts new file mode 100644 index 0000000000..5e8f0e9cbf --- /dev/null +++ b/packages/core/src/renderer/components/kube-object-details/kube-object-detail-items/implementations/validating-webhook-configurations-details-item.injectable.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { kubeObjectDetailItemInjectionToken } from "../kube-object-detail-item-injection-token"; +import { computed } from "mobx"; +import { kubeObjectMatchesToKindAndApiVersion } from "../kube-object-matches-to-kind-and-api-version"; +import currentKubeObjectInDetailsInjectable from "../../current-kube-object-in-details.injectable"; +import { ValidatingWebhookDetails } from "../../../+config-validating-webhook-configurations"; + +const validatingWebhookConfigurationDetailItemInjectable = getInjectable({ + id: "validating-webhook-configuration-detail-item", + + instantiate(di) { + const kubeObject = di.inject(currentKubeObjectInDetailsInjectable); + + return { + Component: ValidatingWebhookDetails, + enabled: computed(() => isValidatingWebhookConfiguration(kubeObject.value.get()?.object)), + orderNumber: 10, + }; + }, + + injectionToken: kubeObjectDetailItemInjectionToken, +}); + +export const isValidatingWebhookConfiguration = kubeObjectMatchesToKindAndApiVersion( + "ValidatingWebhookConfiguration", + ["v1", "admissionregistration.k8s.io/v1beta1", "admissionregistration.k8s.io/v1"], +); + +export default validatingWebhookConfigurationDetailItemInjectable; diff --git a/packages/core/src/renderer/utils/rbac.ts b/packages/core/src/renderer/utils/rbac.ts index 488a8479bf..ac9d4c487a 100644 --- a/packages/core/src/renderer/utils/rbac.ts +++ b/packages/core/src/renderer/utils/rbac.ts @@ -45,6 +45,7 @@ export const ResourceNames: Record = { "serviceaccounts": "Service Accounts", "verticalpodautoscalers": "Vertical Pod Autoscalers", "mutatingwebhookconfigurations": "Mutating Webhook Configurations", + "validatingwebhookconfigurations": "Validating Webhook Configurations", }; export const ResourceKindMap = object.fromEntries( diff --git a/packages/open-lens/integration/__tests__/cluster-pages.tests.ts b/packages/open-lens/integration/__tests__/cluster-pages.tests.ts index 94b2f07b1a..b26997393f 100644 --- a/packages/open-lens/integration/__tests__/cluster-pages.tests.ts +++ b/packages/open-lens/integration/__tests__/cluster-pages.tests.ts @@ -366,6 +366,18 @@ const scenarios = [ parentSidebarItemTestId: null, sidebarItemTestId: "sidebar-item-link-for-custom-resources", }, + + { + expectedSelector: "h5.title", + parentSidebarItemTestId: "sidebar-item-link-for-config", + sidebarItemTestId: "sidebar-item-link-for-validating-webhook-configurations", + }, + + { + expectedSelector: "h5.title", + parentSidebarItemTestId: "sidebar-item-link-for-config", + sidebarItemTestId: "sidebar-item-link-for-mutating-webhook-configurations", + }, ]; const navigateToPods = async (frame: Frame) => {