diff --git a/src/common/k8s-api/endpoints/daemon-set.api.ts b/src/common/k8s-api/endpoints/daemon-set.api.ts index 44c99a13b8..77a58debc9 100644 --- a/src/common/k8s-api/endpoints/daemon-set.api.ts +++ b/src/common/k8s-api/endpoints/daemon-set.api.ts @@ -27,6 +27,7 @@ import { metricsApi } from "./metrics.api"; import type { KubeJsonApiData } from "../kube-json-api"; import type { IPodContainer, IPodMetrics } from "./pods.api"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; +import type { LabelSelector } from "../kube-object"; export class DaemonSet extends WorkloadKubeObject { static kind = "DaemonSet"; @@ -39,11 +40,7 @@ export class DaemonSet extends WorkloadKubeObject { } declare spec: { - selector: { - matchLabels: { - [name: string]: string; - }; - }; + selector: LabelSelector; template: { metadata: { creationTimestamp?: string; diff --git a/src/common/k8s-api/endpoints/deployment.api.ts b/src/common/k8s-api/endpoints/deployment.api.ts index f4d82c64b3..3c748b0abb 100644 --- a/src/common/k8s-api/endpoints/deployment.api.ts +++ b/src/common/k8s-api/endpoints/deployment.api.ts @@ -28,6 +28,7 @@ import { metricsApi } from "./metrics.api"; import type { IPodMetrics } from "./pods.api"; import type { KubeJsonApiData } from "../kube-json-api"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; +import type { LabelSelector } from "../kube-object"; export class DeploymentApi extends KubeApi { protected getScaleApiUrl(params: { namespace: string; name: string }) { @@ -122,7 +123,7 @@ export class Deployment extends WorkloadKubeObject { declare spec: { replicas: number; - selector: { matchLabels: { [app: string]: string }}; + selector: LabelSelector; template: { metadata: { creationTimestamp?: string; diff --git a/src/common/k8s-api/endpoints/job.api.ts b/src/common/k8s-api/endpoints/job.api.ts index 4907e6c796..84ed23cfbe 100644 --- a/src/common/k8s-api/endpoints/job.api.ts +++ b/src/common/k8s-api/endpoints/job.api.ts @@ -27,6 +27,7 @@ import { metricsApi } from "./metrics.api"; import type { KubeJsonApiData } from "../kube-json-api"; import type { IPodContainer, IPodMetrics } from "./pods.api"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; +import type { LabelSelector } from "../kube-object"; export class Job extends WorkloadKubeObject { static kind = "Job"; @@ -42,11 +43,7 @@ export class Job extends WorkloadKubeObject { parallelism?: number; completions?: number; backoffLimit?: number; - selector?: { - matchLabels: { - [name: string]: string; - }; - }; + selector?: LabelSelector; template: { metadata: { creationTimestamp?: string; diff --git a/src/common/k8s-api/endpoints/network-policy.api.ts b/src/common/k8s-api/endpoints/network-policy.api.ts index 1ee1f62371..51fbf95862 100644 --- a/src/common/k8s-api/endpoints/network-policy.api.ts +++ b/src/common/k8s-api/endpoints/network-policy.api.ts @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { KubeObject } from "../kube-object"; +import { KubeObject, LabelSelector } from "../kube-object"; import { autoBind } from "../../utils"; import { KubeApi } from "../kube-api"; import type { KubeJsonApiData } from "../kube-json-api"; @@ -30,46 +30,94 @@ export interface IPolicyIpBlock { except?: string[]; } -export interface IPolicySelector { - matchLabels: { - [label: string]: string; - }; +/** + * @deprecated Use `LabelSelector` instead + */ +export type IPolicySelector = LabelSelector; + +export interface NetworkPolicyPort { + /** + * The protocol which network traffic must match. + * + * One of: + * - `"TCP"` + * - `"UDP"` + * - `"SCTP"` + * + * @default "TCP" + */ + protocol?: string; + + /** + * The port on the given protocol. This can either be a numerical or named + * port on a pod. If this field is not provided, this matches all port names and + * numbers. + * + * If present, only traffic on the specified protocol AND port will be matched. + */ + port?: number | string; + + /** + * If set, indicates that the range of ports from port to endPort, inclusive, + * should be allowed by the policy. This field cannot be defined if the port field + * is not defined or if the port field is defined as a named (string) port. + * + * The endPort must be equal or greater than port. + */ + endPort?: number; +} + +export interface NetworkPolicyPeer { + /** + * IPBlock defines policy on a particular IPBlock. If this field is set then + * neither of the other fields can be. + */ + ipBlock?: IPolicyIpBlock; + + /** + * Selects Namespaces using cluster-scoped labels. This field follows standard label + * selector semantics; if present but empty, it selects all namespaces. + * + * If PodSelector is also set, then the NetworkPolicyPeer as a whole selects + * the Pods matching PodSelector in the Namespaces selected by NamespaceSelector. + * + * Otherwise it selects all Pods in the Namespaces selected by NamespaceSelector. + */ + namespaceSelector?: LabelSelector; + + /** + * This is a label selector which selects Pods. This field follows standard label + * selector semantics; if present but empty, it selects all pods. + * + * If NamespaceSelector is also set, then the NetworkPolicyPeer as a whole selects + * the Pods matching PodSelector in the Namespaces selected by NamespaceSelector. + * + * Otherwise it selects the Pods matching PodSelector in the policy's own Namespace. + */ + podSelector?: LabelSelector; } export interface IPolicyIngress { - from: { - ipBlock?: IPolicyIpBlock; - namespaceSelector?: IPolicySelector; - podSelector?: IPolicySelector; - }[]; - ports: { - protocol: string; - port: number; - }[]; + from?: NetworkPolicyPeer[]; + ports?: NetworkPolicyPort[]; } export interface IPolicyEgress { - to: { - ipBlock: IPolicyIpBlock; - }[]; - ports: { - protocol: string; - port: number; - }[]; + to?: NetworkPolicyPeer[]; + ports?: NetworkPolicyPort[]; +} + +export type PolicyType = "Ingress" | "Egress"; + +export interface NetworkPolicySpec { + podSelector: LabelSelector; + policyTypes?: PolicyType[]; + ingress?: IPolicyIngress[]; + egress?: IPolicyEgress[]; } export interface NetworkPolicy { - spec: { - podSelector: { - matchLabels: { - [label: string]: string; - role: string; - }; - }; - policyTypes: string[]; - ingress: IPolicyIngress[]; - egress: IPolicyEgress[]; - }; + spec: NetworkPolicySpec; } export class NetworkPolicy extends KubeObject { diff --git a/src/common/k8s-api/endpoints/persistent-volume-claims.api.ts b/src/common/k8s-api/endpoints/persistent-volume-claims.api.ts index 6206058e63..1704836478 100644 --- a/src/common/k8s-api/endpoints/persistent-volume-claims.api.ts +++ b/src/common/k8s-api/endpoints/persistent-volume-claims.api.ts @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { KubeObject } from "../kube-object"; +import { KubeObject, LabelSelector } from "../kube-object"; import { autoBind } from "../../utils"; import { IMetrics, metricsApi } from "./metrics.api"; import type { Pod } from "./pods.api"; @@ -51,16 +51,7 @@ export interface PersistentVolumeClaim { spec: { accessModes: string[]; storageClassName: string; - selector: { - matchLabels: { - release: string; - }; - matchExpressions: { - key: string; // environment, - operator: string; // In, - values: string[]; // [dev] - }[]; - }; + selector: LabelSelector; resources: { requests: { storage: string; // 8Gi diff --git a/src/common/k8s-api/endpoints/poddisruptionbudget.api.ts b/src/common/k8s-api/endpoints/poddisruptionbudget.api.ts index b2ac2cd878..dbb85f11ef 100644 --- a/src/common/k8s-api/endpoints/poddisruptionbudget.api.ts +++ b/src/common/k8s-api/endpoints/poddisruptionbudget.api.ts @@ -20,7 +20,7 @@ */ import { autoBind } from "../../utils"; -import { KubeObject } from "../kube-object"; +import { KubeObject, LabelSelector } from "../kube-object"; import { KubeApi } from "../kube-api"; import type { KubeJsonApiData } from "../kube-json-api"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; @@ -29,7 +29,7 @@ export interface PodDisruptionBudget { spec: { minAvailable: string; maxUnavailable: string; - selector: { matchLabels: { [app: string]: string }}; + selector: LabelSelector; }; status: { currentHealthy: number diff --git a/src/common/k8s-api/endpoints/replica-set.api.ts b/src/common/k8s-api/endpoints/replica-set.api.ts index 7a9a76ec06..8876e43da5 100644 --- a/src/common/k8s-api/endpoints/replica-set.api.ts +++ b/src/common/k8s-api/endpoints/replica-set.api.ts @@ -27,6 +27,7 @@ import { metricsApi } from "./metrics.api"; import type { IPodContainer, IPodMetrics, Pod } from "./pods.api"; import type { KubeJsonApiData } from "../kube-json-api"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; +import type { LabelSelector } from "../kube-object"; export class ReplicaSetApi extends KubeApi { protected getScaleApiUrl(params: { namespace: string; name: string }) { @@ -78,7 +79,7 @@ export class ReplicaSet extends WorkloadKubeObject { declare spec: { replicas?: number; - selector: { matchLabels: { [app: string]: string }}; + selector: LabelSelector; template?: { metadata: { labels: { diff --git a/src/common/k8s-api/endpoints/stateful-set.api.ts b/src/common/k8s-api/endpoints/stateful-set.api.ts index dd97e01e05..8e197c69f0 100644 --- a/src/common/k8s-api/endpoints/stateful-set.api.ts +++ b/src/common/k8s-api/endpoints/stateful-set.api.ts @@ -26,6 +26,7 @@ import { metricsApi } from "./metrics.api"; import type { IPodMetrics } from "./pods.api"; import type { KubeJsonApiData } from "../kube-json-api"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; +import type { LabelSelector } from "../kube-object"; export class StatefulSetApi extends KubeApi { protected getScaleApiUrl(params: { namespace: string; name: string }) { @@ -82,11 +83,7 @@ export class StatefulSet extends WorkloadKubeObject { declare spec: { serviceName: string; replicas: number; - selector: { - matchLabels: { - [key: string]: string; - }; - }; + selector: LabelSelector; template: { metadata: { labels: { diff --git a/src/common/k8s-api/kube-object.ts b/src/common/k8s-api/kube-object.ts index 62d1840453..0338472c60 100644 --- a/src/common/k8s-api/kube-object.ts +++ b/src/common/k8s-api/kube-object.ts @@ -104,6 +104,34 @@ export class KubeCreationError extends Error { } } +export type LabelMatchExpression = { + /** + * The label key that the selector applies to. + */ + key: string; +} & ( + { + /** + * This represents the key's relationship to a set of values. + */ + operator: "Exists" | "DoesNotExist"; + values?: undefined; + } + | + { + operator: "In" | "NotIn"; + /** + * The set of values for to match according to the operator for the label. + */ + values: string[]; + } +); + +export interface LabelSelector { + matchLabels?: Record; + matchExpressions?: LabelMatchExpression[]; +} + export class KubeObject implements ItemObject { static readonly kind?: string; static readonly namespaced?: boolean; diff --git a/src/renderer/components/+network-policies/__tests__/network-policy-details.test.tsx b/src/renderer/components/+network-policies/__tests__/network-policy-details.test.tsx new file mode 100644 index 0000000000..ff6adb8cef --- /dev/null +++ b/src/renderer/components/+network-policies/__tests__/network-policy-details.test.tsx @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import React from "react"; +import { findByTestId, findByText, render } from "@testing-library/react"; +import { NetworkPolicy, NetworkPolicySpec } from "../../../../common/k8s-api/endpoints"; +import { NetworkPolicyDetails } from "../network-policy-details"; + +jest.mock("../../kube-object-meta"); + +describe("NetworkPolicyDetails", () => { + it("should render w/o errors", () => { + const policy = new NetworkPolicy({ metadata: {} as any, spec: {}} as any); + const { container } = render(); + + expect(container).toBeInstanceOf(HTMLElement); + }); + + it("should render egress nodeSelector", async () => { + const spec: NetworkPolicySpec = { + egress: [{ + to: [{ + namespaceSelector: { + matchLabels: { + foo: "bar", + }, + }, + }], + }], + podSelector: {}, + }; + const policy = new NetworkPolicy({ metadata: {} as any, spec } as any); + const { container } = render(); + + expect(await findByTestId(container, "egress-0")).toBeInstanceOf(HTMLElement); + expect(await findByText(container, "foo: bar")).toBeInstanceOf(HTMLElement); + }); +}); diff --git a/src/renderer/components/+network-policies/network-policy-details.scss b/src/renderer/components/+network-policies/network-policy-details.module.css similarity index 90% rename from src/renderer/components/+network-policies/network-policy-details.scss rename to src/renderer/components/+network-policies/network-policy-details.module.css index 1bfc8fd384..50be1a6024 100644 --- a/src/renderer/components/+network-policies/network-policy-details.scss +++ b/src/renderer/components/+network-policies/network-policy-details.module.css @@ -20,7 +20,13 @@ */ .NetworkPolicyDetails { - .SubTitle { + .networkPolicyPeerTitle { text-transform: none } -} \ No newline at end of file + + .networkPolicyPeer { + &:not(:last-of-type) { + padding-bottom: 16px; + } + } +} diff --git a/src/renderer/components/+network-policies/network-policy-details.tsx b/src/renderer/components/+network-policies/network-policy-details.tsx index 29cbd45041..e5632ec30d 100644 --- a/src/renderer/components/+network-policies/network-policy-details.tsx +++ b/src/renderer/components/+network-policies/network-policy-details.tsx @@ -19,12 +19,11 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import "./network-policy-details.scss"; +import styles from "./network-policy-details.module.css"; -import get from "lodash/get"; -import React, { Fragment } from "react"; +import React from "react"; import { DrawerItem, DrawerTitle } from "../drawer"; -import { IPolicyEgress, IPolicyIngress, IPolicyIpBlock, IPolicySelector, NetworkPolicy } from "../../../common/k8s-api/endpoints/network-policy.api"; +import { IPolicyIpBlock, IPolicySelector, NetworkPolicy, NetworkPolicyPeer, NetworkPolicyPort } from "../../../common/k8s-api/endpoints/network-policy.api"; import { Badge } from "../badge"; import { SubTitle } from "../layout/sub-title"; import { observer } from "mobx-react"; @@ -37,78 +36,84 @@ interface Props extends KubeObjectDetailsProps { @observer export class NetworkPolicyDetails extends React.Component { - renderIngressFrom(ingress: IPolicyIngress) { - const { from } = ingress; + renderIPolicyIpBlock(ipBlock: IPolicyIpBlock | undefined) { + if (!ipBlock) { + return null; + } - if (!from) return null; + const { cidr, except = [] } = ipBlock; + + if (!cidr) { + return null; + } + + const items = [`cidr: ${cidr}`]; + + if (except.length > 0) { + items.push(`except: ${except.join(", ")}`); + } + + return ( + + {items.join(", ")} + + ); + } + + renderIPolicySelector(name: string, selector: IPolicySelector | undefined) { + if (!selector) { + return null; + } + + return ( + + { + Object + .entries(selector.matchLabels) + .map(data => data.join(": ")) + .join(", ") + || "(empty)" + } + + ); + } + + renderNetworkPolicyPeers(name: string, peers: NetworkPolicyPeer[] | undefined) { + if (!peers) { + return null; + } return ( <> - - {from.map(item => - Object.keys(item).map(key => { - const data = get(item, key); - - if (key === "ipBlock") { - const { cidr, except } = data as IPolicyIpBlock; - - if (!cidr) return null; - - return ( - - cidr: {cidr}, {" "} - {except && - `except: ${except.join(", ")}` - } - - ); - } - const selector: IPolicySelector = data; - - if (selector.matchLabels) { - return ( - - { - Object - .entries(selector.matchLabels) - .map(data => data.join(": ")) - .join(", ") - } - - ); - } - else { - return ((empty)); - } - }), - )} + + { + peers.map((peer, index) => ( +
+ {this.renderIPolicyIpBlock(peer.ipBlock)} + {this.renderIPolicySelector("namespaceSelector", peer.namespaceSelector)} + {this.renderIPolicySelector("podSelector", peer.podSelector)} +
+ )) + } ); } - renderEgressTo(egress: IPolicyEgress) { - const { to } = egress; - - if (!to) return null; + renderNetworkPolicyPorts(ports: NetworkPolicyPort[] | undefined) { + if (!ports) { + return null; + } return ( - <> - - {to.map(item => { - const { ipBlock: { cidr, except } = {}} = item; - - if (!cidr) return null; - - return ( - - cidr: {cidr}, {" "} - {except && - `except: ${except.join(", ")}` - } - - ); - })} - + +
    + {ports.map(({ protocol = "TCP", port = "", endPort }, index) => ( +
  • + {protocol}:{port}{typeof endPort === "number" && `:${endPort}`} +
  • + ))} +
+
); } @@ -129,49 +134,38 @@ export class NetworkPolicyDetails extends React.Component { const selector = policy.getMatchLabels(); return ( -
+
0}> - {selector.length > 0 ? - policy.getMatchLabels().map(label => ) : - `(empty) (Allowing the specific traffic to all pods in this namespace)` + { + selector.length > 0 + ? policy.getMatchLabels().map(label => ) + : `(empty) (Allowing the specific traffic to all pods in this namespace)` } {ingress && ( <> - {ingress.map((ingress, i) => { - const { ports } = ingress; - - return ( - - - {ports && ports.map(({ port, protocol }) => `${protocol || ""}:${port || ""}`).join(", ")} - - {this.renderIngressFrom(ingress)} - - ); - })} + {ingress.map((ingress, i) => ( +
+ {this.renderNetworkPolicyPorts(ingress.ports)} + {this.renderNetworkPolicyPeers("From", ingress.from)} +
+ ))} )} {egress && ( <> - {egress.map((egress, i) => { - const { ports } = egress; - - return ( - - - {ports && ports.map(({ port, protocol }) => `${protocol || ""}:${port || ""}`).join(", ")} - - {this.renderEgressTo(egress)} - - ); - })} + {egress.map((egress, i) => ( +
+ {this.renderNetworkPolicyPorts(egress.ports)} + {this.renderNetworkPolicyPeers("To", egress.to)} +
+ ))} )}