diff --git a/.mk/static.mk b/.mk/static.mk new file mode 100644 index 000000000..caea410bf --- /dev/null +++ b/.mk/static.mk @@ -0,0 +1,6 @@ +##@ Static + +.PHONY: build-frontend-static +build-frontend-static: install-frontend fmt-frontend ## Run npm install, format and build static frontend + @echo "### Building static frontend" + cd web && npm run build:static \ No newline at end of file diff --git a/Dockerfile.cypress b/Dockerfile.cypress index e3217be8a..d5fb69865 100644 --- a/Dockerfile.cypress +++ b/Dockerfile.cypress @@ -19,6 +19,7 @@ COPY mocks mocks WORKDIR /opt/app-root/web RUN npm run format-all RUN npm run build$BUILDSCRIPT +RUN npm run build:static FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.24 as go-builder diff --git a/Dockerfile.downstream b/Dockerfile.downstream index 00d37d2d6..0664f1048 100644 --- a/Dockerfile.downstream +++ b/Dockerfile.downstream @@ -17,6 +17,7 @@ COPY --chown=default mocks mocks WORKDIR /opt/app-root/web RUN npm run format-all RUN npm run build +RUN npm run build:static FROM brew.registry.redhat.io/rh-osbs/openshift-golang-builder:v1.24 as go-builder diff --git a/Dockerfile.front b/Dockerfile.front index 8a9e19f70..67138503d 100644 --- a/Dockerfile.front +++ b/Dockerfile.front @@ -17,6 +17,7 @@ COPY mocks mocks WORKDIR /opt/app-root/web RUN npm run format-all RUN npm run build$BUILDSCRIPT +RUN npm run build:static FROM scratch diff --git a/Makefile b/Makefile index d274940df..8ba12dd0d 100644 --- a/Makefile +++ b/Makefile @@ -228,3 +228,4 @@ endif include .mk/cypress.mk include .mk/shortcuts.mk include .mk/standalone.mk +include .mk/static.mk diff --git a/pkg/config/config.go b/pkg/config/config.go index 1cd2c844e..cd7e92b9d 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -144,12 +144,15 @@ type Config struct { Frontend Frontend `yaml:"frontend" json:"frontend"` Server Server `yaml:"server,omitempty" json:"server,omitempty"` Path string `yaml:"-" json:"-"` + Static bool } func ReadFile(version, date, filename string) (*Config, error) { + isStatic := len(filename) == 0 // set default values cfg := Config{ - Path: filename, + Path: filename, + Static: isStatic, Server: Server{ Port: 9001, MetricsPort: 9002, @@ -191,7 +194,10 @@ func ReadFile(version, date, filename string) (*Config, error) { PromLabels: []string{}, }, } - if len(filename) == 0 { + if isStatic { + // Force TLS + cfg.Server.CertPath = "/var/serving-cert/tls.crt" + cfg.Server.KeyPath = "/var/serving-cert/tls.key" return &cfg, nil } yamlFile, err := os.ReadFile(filename) @@ -241,7 +247,7 @@ func (c *Config) IsPromEnabled() bool { } func (c *Config) Validate() error { - if !c.IsLokiEnabled() && !c.IsPromEnabled() { + if !c.Static && !c.IsLokiEnabled() && !c.IsPromEnabled() { return errors.New("neither Loki nor Prometheus is configured; at least one of them should have a URL defined") } diff --git a/pkg/server/routes.go b/pkg/server/routes.go index 147150fc5..7cafd6c61 100644 --- a/pkg/server/routes.go +++ b/pkg/server/routes.go @@ -41,31 +41,37 @@ func setupRoutes(ctx context.Context, cfg *config.Config, authChecker auth.Check }) }) - // Server status + // Server status and config api.HandleFunc("/status", h.Status(ctx)) + api.HandleFunc("/frontend-config", h.GetFrontendConfig()) - // Loki endpoints - api.HandleFunc("/loki/ready", h.LokiReady()) - api.HandleFunc("/loki/metrics", forceCheckAdmin(authChecker, h.LokiMetrics())) - api.HandleFunc("/loki/buildinfo", forceCheckAdmin(authChecker, h.LokiBuildInfos())) - api.HandleFunc("/loki/config/limits", forceCheckAdmin(authChecker, h.LokiLimits())) - api.HandleFunc("/loki/flow/records", h.GetFlows(ctx)) - api.HandleFunc("/loki/export", h.ExportFlows(ctx)) + if cfg.Static { + // Expose static files only + r.PathPrefix("/").Handler(http.FileServer(http.Dir("./web/dist/static"))) + } else { + // Loki endpoints + api.HandleFunc("/loki/ready", h.LokiReady()) + api.HandleFunc("/loki/metrics", forceCheckAdmin(authChecker, h.LokiMetrics())) + api.HandleFunc("/loki/buildinfo", forceCheckAdmin(authChecker, h.LokiBuildInfos())) + api.HandleFunc("/loki/config/limits", forceCheckAdmin(authChecker, h.LokiLimits())) + api.HandleFunc("/loki/flow/records", h.GetFlows(ctx)) + api.HandleFunc("/loki/export", h.ExportFlows(ctx)) - // Common endpoints - api.HandleFunc("/flow/metrics", h.GetTopology(ctx)) - api.HandleFunc("/resources/clusters", h.GetClusters(ctx)) - api.HandleFunc("/resources/udns", h.GetUDNs(ctx)) - api.HandleFunc("/resources/zones", h.GetZones(ctx)) - api.HandleFunc("/resources/namespaces", h.GetNamespaces(ctx)) - api.HandleFunc("/resources/names", h.GetNames(ctx)) + // Common endpoints + api.HandleFunc("/flow/metrics", h.GetTopology(ctx)) + api.HandleFunc("/resources/clusters", h.GetClusters(ctx)) + api.HandleFunc("/resources/udns", h.GetUDNs(ctx)) + api.HandleFunc("/resources/zones", h.GetZones(ctx)) + api.HandleFunc("/resources/namespaces", h.GetNamespaces(ctx)) + api.HandleFunc("/resources/names", h.GetNames(ctx)) - // K8S endpoints - api.HandleFunc("/k8s/resources/udnIds", h.GetUDNIdss(ctx)) + // K8S endpoints + api.HandleFunc("/k8s/resources/udnIds", h.GetUDNIdss(ctx)) + + // Frontend files + r.PathPrefix("/").Handler(http.FileServer(http.Dir("./web/dist/"))) + } - // Frontend files - api.HandleFunc("/frontend-config", h.GetFrontendConfig()) - r.PathPrefix("/").Handler(http.FileServer(http.Dir("./web/dist/"))) return r } diff --git a/web/console-extensions.json b/web/console-extensions.json deleted file mode 100644 index 2a439507b..000000000 --- a/web/console-extensions.json +++ /dev/null @@ -1,281 +0,0 @@ -[ - { - "type": "console.navigation/href", - "properties": { - "id": "netflow-traffic-link", - "perspective": "admin", - "section": "observe", - "name": "%plugin__netobserv-plugin~Network Traffic%", - "href": "/netflow-traffic" - } - }, - { - "type": "console.navigation/href", - "properties": { - "id": "netflow-traffic-link-projectadmin", - "perspective": "admin", - "section": "observe-projectadmin", - "name": "%plugin__netobserv-plugin~Network Traffic%", - "href": "/netflow-traffic" - }, - "flags": { "disallowed": ["CAN_LIST_NS"] } - }, - { - "type": "console.page/route", - "properties": { - "path": "/netflow-traffic", - "component": { - "$codeRef": "netflowParent.default" - } - } - }, - { - "type": "console.tab/horizontalNav", - "properties": { - "model": { - "version": "v1", - "group": "", - "kind": "Pod" - }, - "component": { - "$codeRef": "netflowTab.default" - }, - "page": { - "name": "%plugin__netobserv-plugin~Network Traffic%", - "href": "netflow" - } - } - }, - { - "type": "console.tab/horizontalNav", - "properties": { - "model": { - "version": "v1", - "group": "", - "kind": "Service" - }, - "component": { - "$codeRef": "netflowTab.default" - }, - "page": { - "name": "%plugin__netobserv-plugin~Network Traffic%", - "href": "netflow" - } - } - }, - { - "type": "console.tab/horizontalNav", - "properties": { - "model": { - "version": "v1", - "group": "", - "kind": "Namespace" - }, - "component": { - "$codeRef": "netflowTab.default" - }, - "page": { - "name": "%plugin__netobserv-plugin~Network Traffic%", - "href": "netflow" - } - } - }, - { - "type": "console.tab/horizontalNav", - "properties": { - "model": { - "version": "v1", - "group": "apps", - "kind": "Deployment" - }, - "component": { - "$codeRef": "netflowTab.default" - }, - "page": { - "name": "%plugin__netobserv-plugin~Network Traffic%", - "href": "netflow" - } - } - }, - { - "type": "console.tab/horizontalNav", - "properties": { - "model": { - "version": "v1", - "group": "apps", - "kind": "StatefulSet" - }, - "component": { - "$codeRef": "netflowTab.default" - }, - "page": { - "name": "%plugin__netobserv-plugin~Network Traffic%", - "href": "netflow" - } - } - }, - { - "type": "console.tab/horizontalNav", - "properties": { - "model": { - "version": "v1", - "group": "apps", - "kind": "DaemonSet" - }, - "component": { - "$codeRef": "netflowTab.default" - }, - "page": { - "name": "%plugin__netobserv-plugin~Network Traffic%", - "href": "netflow" - } - } - }, - { - "type": "console.tab/horizontalNav", - "properties": { - "model": { - "version": "v1", - "group": "apps", - "kind": "ReplicaSet" - }, - "component": { - "$codeRef": "netflowTab.default" - }, - "page": { - "name": "%plugin__netobserv-plugin~Network Traffic%", - "href": "netflow" - } - } - }, - { - "type": "console.tab/horizontalNav", - "properties": { - "model": { - "version": "v1", - "group": "", - "kind": "Node" - }, - "component": { - "$codeRef": "netflowTab.default" - }, - "page": { - "name": "%plugin__netobserv-plugin~Network Traffic%", - "href": "netflow" - } - } - }, - { - "type": "console.tab/horizontalNav", - "properties": { - "model": { - "version": "v1", - "group": "batch", - "kind": "CronJob" - }, - "component": { - "$codeRef": "netflowTab.default" - }, - "page": { - "name": "%plugin__netobserv-plugin~Network Traffic%", - "href": "netflow" - } - } - }, - { - "type": "console.tab/horizontalNav", - "properties": { - "model": { - "version": "v1", - "group": "batch", - "kind": "Job" - }, - "component": { - "$codeRef": "netflowTab.default" - }, - "page": { - "name": "%plugin__netobserv-plugin~Network Traffic%", - "href": "netflow" - } - } - }, - { - "type": "console.tab/horizontalNav", - "properties": { - "model": { - "version": "v2beta2", - "group": "autoscaling", - "kind": "HorizontalPodAutoscaler" - }, - "component": { - "$codeRef": "netflowTab.default" - }, - "page": { - "name": "%plugin__netobserv-plugin~Network Traffic%", - "href": "netflow" - } - } - }, - { - "type": "console.tab/horizontalNav", - "properties": { - "model": { - "version": "v1", - "group": "route.openshift.io", - "kind": "Route" - }, - "component": { - "$codeRef": "netflowTab.default" - }, - "page": { - "name": "%plugin__netobserv-plugin~Network Traffic%", - "href": "netflow" - } - } - }, - { - "type": "console.tab/horizontalNav", - "properties": { - "model": { - "version": "v1", - "group": "k8s.ovn.org", - "kind": "ClusterUserDefinedNetwork" - }, - "component": { - "$codeRef": "netflowTab.default" - }, - "page": { - "name": "%plugin__netobserv-plugin~Network Traffic%", - "href": "netflow" - } - } - }, - { - "type": "console.tab/horizontalNav", - "properties": { - "model": { - "version": "v1", - "group": "k8s.ovn.org", - "kind": "UserDefinedNetwork" - }, - "component": { - "$codeRef": "netflowTab.default" - }, - "page": { - "name": "%plugin__netobserv-plugin~Network Traffic%", - "href": "netflow" - } - } - }, - { - "type": "console.tab", - "properties": { - "contextId": "dev-console-observe", - "name": "%plugin__netobserv-plugin~Network Traffic%", - "href": "netflow-traffic", - "component": { - "$codeRef": "netflowDevTab.default" - } - } - } -] \ No newline at end of file diff --git a/web/locales/en/plugin__netobserv-plugin.json b/web/locales/en/plugin__netobserv-plugin.json index 6b688be50..e245d4370 100644 --- a/web/locales/en/plugin__netobserv-plugin.json +++ b/web/locales/en/plugin__netobserv-plugin.json @@ -162,6 +162,65 @@ "S": "S", "XS": "XS", "None": "None", + "Cluster metrics": "Cluster metrics", + "Bandwidth": "Bandwidth", + "Nodes": "Nodes", + "Namespaces": "Namespaces", + "Pods": "Pods", + "Recommendations": "Recommendations", + "vCPU": "vCPU", + "Memory": "Memory", + "LokiStack size": "LokiStack size", + "Kafka": "Kafka", + "Estimation": "Estimation", + "Sampling": "Sampling", + "(current)": "(current)", + "There is some issue in this form view. Please select \"YAML view\" for full control.": "There is some issue in this form view. Please select \"YAML view\" for full control.", + "Note: Some fields may not be represented in this form view. Please select \"YAML view\" for full control.": "Note: Some fields may not be represented in this form view. Please select \"YAML view\" for full control.", + "Remove {{singularLabel}}": "Remove {{singularLabel}}", + "Add {{singularLabel}}": "Add {{singularLabel}}", + "Error": "Error", + "Fix the following errors:": "Fix the following errors:", + "Enabled": "Enabled", + "Disabled": "Disabled", + "Select {{title}}": "Select {{title}}", + "Configure via:": "Configure via:", + "Form view": "Form view", + "YAML view": "YAML view", + "This object has been updated.": "This object has been updated.", + "Click reload to see the new version.": "Click reload to see the new version.", + "Update": "Update", + "Create": "Create", + "Reload": "Reload", + "Cancel": "Cancel", + "Network Observability FlowCollector status": "Network Observability FlowCollector status", + "Network Observability FlowCollector setup": "Network Observability FlowCollector setup", + "Overview": "Overview", + "Network Observability Operator deploys a monitoring pipeline that consists in:\n - an eBPF agent, that generates network flows from captured packets\n - flowlogs-pipeline, a component that collects, enriches and exports these flows\n - a Console plugin for flows visualization with powerful filtering options, a topology representation and more\n\nFlow data is then available in multiple ways, each optional:\n - As Prometheus metrics\n - As raw flow logs stored in Grafana Loki\n - As raw flow logs exported to a collector\n\nThe FlowCollector resource is used to configure the operator and its managed components.\nThis setup will guide you on the common aspects of the FlowCollector configuration.": "Network Observability Operator deploys a monitoring pipeline that consists in:\n - an eBPF agent, that generates network flows from captured packets\n - flowlogs-pipeline, a component that collects, enriches and exports these flows\n - a Console plugin for flows visualization with powerful filtering options, a topology representation and more\n\nFlow data is then available in multiple ways, each optional:\n - As Prometheus metrics\n - As raw flow logs stored in Grafana Loki\n - As raw flow logs exported to a collector\n\nThe FlowCollector resource is used to configure the operator and its managed components.\nThis setup will guide you on the common aspects of the FlowCollector configuration.", + "Operator configuration": "Operator configuration", + "Capture": "Capture", + "Pipeline": "Pipeline", + "Storage": "Storage", + "Integration": "Integration", + "Consumption": "Consumption", + "Review": "Review", + "Network Observability FlowMetric setup": "Network Observability FlowMetric setup", + "You can create custom metrics out of the flowlogs data using the FlowMetric API. In every flowlogs data that is collected, there are a number of fields labeled per log, such as source name and destination name. These fields can be leveraged as Prometheus labels to enable the customization of cluster information on your dashboard.\nThis setup will guide you on the common aspects of the FlowMetric configuration.": "You can create custom metrics out of the flowlogs data using the FlowMetric API. In every flowlogs data that is collected, there are a number of fields labeled per log, such as source name and destination name. These fields can be leveraged as Prometheus labels to enable the customization of cluster information on your dashboard.\nThis setup will guide you on the common aspects of the FlowMetric configuration.", + "General configuration": "General configuration", + "Metric": "Metric", + "Data": "Data", + "Charts": "Charts", + "Update {{kind}}": "Update {{kind}}", + "Create {{kind}}": "Create {{kind}}", + "Update by completing the form. Current values are from the existing resource.": "Update by completing the form. Current values are from the existing resource.", + "Create by completing the form. Default values are provided as example.": "Create by completing the form. Default values are provided as example.", + "{{kind}} resource doesn't exists yet.": "{{kind}} resource doesn't exists yet.", + "Type": "Type", + "Status": "Status", + "Reason": "Reason", + "Message": "Message", + "Changed": "Changed", + "Unable to get {{kind}}": "Unable to get {{kind}}", "Step {{step}}/{{total}}": "Step {{step}}/{{total}}", "Previous tip": "Previous tip", "Next tip": "Next tip", @@ -233,7 +292,6 @@ "Unselect all": "Unselect all", "Select all": "Select all", "Restore default columns": "Restore default columns", - "Cancel": "Cancel", "At least one column must be selected": "At least one column must be selected", "Save": "Save", "Export": "Export", @@ -298,7 +356,6 @@ "from": "from", "in": "in", "Configuration": "Configuration", - "Sampling": "Sampling", "Max chunk age": "Max chunk age", "Version": "Version", "Number": "Number", @@ -348,7 +405,6 @@ "Pin this element": "Pin this element", "Could not fetch drop information": "Could not fetch drop information", "Sorry, 3D view is not implemented anymore.": "Sorry, 3D view is not implemented anymore.", - "Overview": "Overview", "Traffic flows": "Traffic flows", "Topology": "Topology", "Hide histogram": "Hide histogram", diff --git a/web/moduleMapper/dummy.tsx b/web/moduleMapper/dummy.tsx index 2477cd4a9..74213de5e 100644 --- a/web/moduleMapper/dummy.tsx +++ b/web/moduleMapper/dummy.tsx @@ -1,6 +1,24 @@ -import { NamespaceBarProps, ResourceIconProps, ResourceLinkProps } from '@openshift-console/dynamic-plugin-sdk'; +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + K8sGroupVersionKind, + K8sModel, + K8sResourceKind, + K8sResourceKindReference, + NamespaceBarProps, + PrometheusPollProps, + PrometheusResponse, + ResourceIconProps, + ResourceLinkProps, + ResourceYAMLEditorProps +} from '@openshift-console/dynamic-plugin-sdk'; +import { CodeEditor, Language } from '@patternfly/react-code-editor'; +import _ from 'lodash'; import * as React from 'react'; +import { GetFlowCollectorJS } from '../src/components/forms/config/templates'; import { useK8sModelsWithColors } from '../src/utils/k8s-models-hook'; +import { useTheme } from '../src/utils/theme-hook'; +import { safeJSToYAML } from '../src/utils/yaml'; import { k8sModels } from './k8s-models'; // This dummy file is used to resolve @Console imports from @openshift-console for JEST / Standalone @@ -35,6 +53,140 @@ export function useK8sModels() { ] } +export function getK8sModel(k8s: any, k8sGroupVersionKind?: K8sResourceKindReference | K8sGroupVersionKind): K8sModel { + const models = Object.keys(k8sModels); + + for (let i = 0; i < models.length; i++) { + const model = (k8sModels as any)[models[i]]; + if (model.kind === k8s.kind) { + return model; + } + } + + return { + abbr: '', + kind: '', + label: '', + labelPlural: '', + plural: '', + apiVersion: '' + }; +} + +export function k8sGet(k8s: any): Promise { + console.log("k8sGet", k8s); + return Promise.resolve(k8s); +} + +export function k8sCreate(k8s: any): Promise { + console.log("k8sCreate", k8s); + return Promise.resolve(k8s); +} + +export function k8sUpdate(k8s: any): Promise { + console.log("k8sUpdate", k8s); + return Promise.resolve(k8s); +} + +export function useK8sWatchResource(req: any) { + console.log("useK8sWatchResource", req); + + const [loaded, setLoaded] = React.useState(false); + const [resource, setResource] = React.useState(null); + + React.useEffect(() => { + // simulate a loading + if (resource == null) { + setTimeout(() => { + switch (req.groupVersionKind.kind) { + case 'FlowCollector': + const fc = _.cloneDeep(GetFlowCollectorJS()); + fc.spec!.loki.enable = false; + fc.spec!.exporters = [{ type: "Kafka" }, { type: "OpenTelemetry" }] + fc.status = { + "conditions": [ + { + "lastTransitionTime": "2025-04-08T09:01:44Z", + "message": "4 ready components, 0 with failure, 1 pending", + "reason": "Pending", + "status": "False", + "type": "Ready" + }, + { + "lastTransitionTime": "2025-04-08T09:01:44Z", + "message": "Deployment netobserv-plugin not ready: 1/1 (Deployment does not have minimum availability.)", + "reason": "DeploymentNotReady", + "status": "True", + "type": "WaitingFlowCollectorLegacy" + }, + { + "lastTransitionTime": "2025-04-08T09:01:44Z", + "message": "", + "reason": "Ready", + "status": "False", + "type": "WaitingMonitoring" + }, + { + "lastTransitionTime": "2025-04-08T09:01:43Z", + "message": "", + "reason": "Ready", + "status": "False", + "type": "WaitingNetworkPolicy" + }, + { + "lastTransitionTime": "2025-04-08T09:01:43Z", + "message": "", + "reason": "Valid", + "status": "False", + "type": "ConfigurationIssue" + }, + { + "lastTransitionTime": "2025-04-08T09:01:43Z", + "message": "Loki is not configured in LokiStack mode", + "reason": "Unused", + "status": "Unknown", + "type": "LokiIssue" + }, + { + "lastTransitionTime": "2025-04-08T09:01:45Z", + "message": "", + "reason": "Ready", + "status": "False", + "type": "WaitingFLPParent" + }, + { + "lastTransitionTime": "2025-04-08T09:01:45Z", + "message": "", + "reason": "Ready", + "status": "False", + "type": "WaitingFLPMonolith" + }, + { + "lastTransitionTime": "2025-04-08T09:01:44Z", + "message": "Transformer only used with Kafka", + "reason": "ComponentUnused", + "status": "Unknown", + "type": "WaitingFLPTransformer" + } + ] + } + setResource(fc); + break; + } + setLoaded(true); + }, 1000); + } + }, [req.groupVersionKind.kind, req.kind, resource]); + + return React.useMemo(() => { + if (!resource) { + return [null, loaded, null]; + } else { + return [[resource], loaded, null]; + } + }, [loaded, resource]); +} + export const ResourceIcon: React.FC = ({ className, kind, @@ -89,3 +241,103 @@ export const NamespaceBar: React.FC = ({
{children}
) }; + +export const ResourceYAMLEditor: React.FC = ({ + initialResource, + header, + onSave, +}) => { + const isDarkTheme = useTheme(); + const containerHeight = document.getElementById("editor-content-container")?.clientHeight || 800; + const footerHeight = document.getElementById("editor-toggle-footer")?.clientHeight || 0; + return (<> + onSave && onSave(value)} + /> + ); +}; + +export enum K8sResourceConditionStatus { + True = "True", + False = "False", + Unknown = "Unknown" +} + +export enum PrometheusEndpoint { + LABEL = "api/v1/label", + QUERY = "api/v1/query", + QUERY_RANGE = "api/v1/query_range", + RULES = "api/v1/rules", + TARGETS = "api/v1/targets" +} + +export function usePrometheusPoll(props: PrometheusPollProps) { + console.log("usePrometheusPoll", props); + + const [response, setResponse] = React.useState(null); + + React.useEffect(() => { + // simulate a loading + if (response == null) { + setTimeout(() => { + setResponse({ + status: "success", + data: { + resultType: "vector", + result: [ + { + metric: { + node: "node-1", + namespace: "ns-1", + pod: "pod-1", + }, + value: [ + 1745832954.698, + "2000" + ] + }, + { + metric: { + node: "node-2", + namespace: "ns-2", + pod: "pod-1", + }, + value: [ + 1745832954.698, + "100" + ] + }, + { + metric: { + node: "node-3", + namespace: "ns-1", + pod: "pod-1", + }, + value: [ + 1745832954.698, + "400" + ] + }, + ], + } + }); + }, 1000); + } + }, [response]); + + return React.useMemo(() => { + if (response == null) { + return [null, false, null]; + } else { + return [response, true, null]; + } + }, [response]); +} diff --git a/web/package-lock.json b/web/package-lock.json index e6461c292..52e021d6e 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -11,16 +11,20 @@ "@openshift-console/plugin-shared": "^0.0.3", "@patternfly/patternfly": "^5.4.0", "@patternfly/react-charts": "^7.4.0", + "@patternfly/react-code-editor": "^5.4.0", "@patternfly/react-core": "^5.4.0", "@patternfly/react-icons": "^5.4.0", "@patternfly/react-styles": "^5.4.0", "@patternfly/react-table": "^5.4.0", "@patternfly/react-tokens": "^5.4.0", "@patternfly/react-topology": "^5.4.0", + "@rjsf/core": "^5.24.12", + "@rjsf/validator-ajv8": "^5.24.12", "axios": "^1.8.2", "html-to-image": "^1.11.11", "i18next": "^21.8.14", "i18next-http-backend": "^1.0.21", + "js-yaml": "^4.1.0", "murmurhash-js": "^1.0.0", "percentile": "^1.6.0", "port-numbers": "^6.0.1", @@ -44,8 +48,9 @@ "@types/copy-webpack-plugin": "8.0.1", "@types/enzyme": "3.10.x", "@types/jest": "30.0.0", + "@types/js-yaml": "^4.0.9", "@types/jsdom": "^16.2.13", - "@types/lodash": "^4.14.178", + "@types/lodash": "^4.17.20", "@types/murmurhash-js": "^1.0.3", "@types/port-numbers": "^5.0.0", "@types/react": "^17.0.1", @@ -4485,6 +4490,29 @@ "dev": true, "license": "MIT" }, + "node_modules/@monaco-editor/loader": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.5.0.tgz", + "integrity": "sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw==", + "license": "MIT", + "dependencies": { + "state-local": "^1.0.6" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", + "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", + "license": "MIT", + "dependencies": { + "@monaco-editor/loader": "^1.5.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -4708,11 +4736,54 @@ "react-dom": "^17 || ^18" } }, + "node_modules/@patternfly/react-code-editor": { + "version": "5.4.18", + "resolved": "https://registry.npmjs.org/@patternfly/react-code-editor/-/react-code-editor-5.4.18.tgz", + "integrity": "sha512-xFtdRj9NwHhi1pfIQgj2hrvDqjMi3pLiRKhdI+TcUjnRJQygjZdEvklsEHHoZmUELsNf/98yrwsWt0iLOzdvdQ==", + "dependencies": { + "@monaco-editor/react": "^4.6.0", + "@patternfly/react-core": "^5.4.14", + "@patternfly/react-icons": "^5.4.2", + "@patternfly/react-styles": "^5.4.1", + "react-dropzone": "14.2.3", + "tslib": "^2.7.0" + }, + "peerDependencies": { + "react": "^17 || ^18", + "react-dom": "^17 || ^18" + } + }, + "node_modules/@patternfly/react-code-editor/node_modules/file-selector": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz", + "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==", + "dependencies": { + "tslib": "^2.4.0" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@patternfly/react-code-editor/node_modules/react-dropzone": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz", + "integrity": "sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==", + "dependencies": { + "attr-accept": "^2.2.2", + "file-selector": "^0.6.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "react": ">= 16.8 || 18.0.0" + } + }, "node_modules/@patternfly/react-core": { - "version": "5.4.10", - "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-5.4.10.tgz", - "integrity": "sha512-lThdXKqHc9sN5AqPskZa1YmUmSwbjdkaT8neTCdMjfnBaBanYCmSCNUMYUsoB0L/2siu2Vlcp12RRaG5hZeN2g==", - "license": "MIT", + "version": "5.4.14", + "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-5.4.14.tgz", + "integrity": "sha512-oXVMzLs9Pa+xmdc39L2u05zbXfY3mWuOFi4GDv44GPdDexZUFy5W69+Nv5P8cwfMim55Nf5kKYpcqmatD2bBXw==", "dependencies": { "@patternfly/react-icons": "^5.4.2", "@patternfly/react-styles": "^5.4.1", @@ -4874,6 +4945,93 @@ "node": ">=14.0.0" } }, + "node_modules/@rjsf/core": { + "version": "5.24.12", + "resolved": "https://registry.npmjs.org/@rjsf/core/-/core-5.24.12.tgz", + "integrity": "sha512-OWVdC501n3Io0hplgpnkzArpcUSiImMgLQhk6/EI8wu2xbvk5fTiM7YAVlAObpAD3z3LRrAwhjnmh9L4k/FWmQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "markdown-to-jsx": "^7.4.1", + "nanoid": "^3.3.7", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@rjsf/utils": "^5.24.x", + "react": "^16.14.0 || >=17" + } + }, + "node_modules/@rjsf/utils": { + "version": "5.24.12", + "resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-5.24.12.tgz", + "integrity": "sha512-fDwQB0XkjZjpdFUz5UAnuZj8nnbxDbX5tp+jTOjjJKw2TMQ9gFFYCQ12lSpdhezA2YgEGZfxyYTGW0DKDL5Drg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "json-schema-merge-allof": "^0.8.1", + "jsonpointer": "^5.0.1", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "react-is": "^18.2.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.14.0 || >=17" + } + }, + "node_modules/@rjsf/utils/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT", + "peer": true + }, + "node_modules/@rjsf/validator-ajv8": { + "version": "5.24.12", + "resolved": "https://registry.npmjs.org/@rjsf/validator-ajv8/-/validator-ajv8-5.24.12.tgz", + "integrity": "sha512-IMXdCjvDNdvb+mDgZC3AlAtr0pjYKq5s0GcLECjG5PuiX7Ib4JaDQHZY5ZJdKblMfgzhsn8AAOi573jXAt7BHQ==", + "license": "Apache-2.0", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^2.1.1", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@rjsf/utils": "^5.24.x" + } + }, + "node_modules/@rjsf/validator-ajv8/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@rjsf/validator-ajv8/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -5615,6 +5773,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/jsdom": { "version": "16.2.13", "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-16.2.13.tgz", @@ -5633,10 +5798,11 @@ "dev": true }, "node_modules/@types/lodash": { - "version": "4.14.178", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.178.tgz", - "integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==", - "dev": true + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", + "dev": true, + "license": "MIT" }, "node_modules/@types/mime": { "version": "1.3.5", @@ -6755,7 +6921,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, "dependencies": { "ajv": "^8.0.0" }, @@ -6772,7 +6937,6 @@ "version": "8.10.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.10.0.tgz", "integrity": "sha512-bzqAEZOjkrUMl2afH8dknrq5KEk2SrwdBROR+vH1EKVQTqaUbJVPdc/gEdggTMM0Se+s+Ja4ju4TlNcStKl2Hw==", - "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -6787,8 +6951,7 @@ "node_modules/ajv-formats/node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "node_modules/ajv-keywords": { "version": "3.5.2", @@ -6917,8 +7080,7 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/aria-query": { "version": "5.0.0", @@ -8483,6 +8645,29 @@ } ] }, + "node_modules/compute-gcd": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/compute-gcd/-/compute-gcd-1.2.1.tgz", + "integrity": "sha512-TwMbxBNz0l71+8Sc4czv13h4kEqnchV9igQZBi6QUaz09dnz13juGnnaWWJTRsP3brxOoxeB4SA2WELLw1hCtg==", + "peer": true, + "dependencies": { + "validate.io-array": "^1.0.3", + "validate.io-function": "^1.0.2", + "validate.io-integer-array": "^1.0.0" + } + }, + "node_modules/compute-lcm": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/compute-lcm/-/compute-lcm-1.1.2.tgz", + "integrity": "sha512-OFNPdQAXnQhDSKioX8/XYT6sdUlXwpeMjfd6ApxMJfyZ4GxmLR1xvMERctlYhlHwIiz6CSpBc2+qYKjHGZw4TQ==", + "peer": true, + "dependencies": { + "compute-gcd": "^1.2.1", + "validate.io-array": "^1.0.3", + "validate.io-function": "^1.0.2", + "validate.io-integer-array": "^1.0.0" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -11246,8 +11431,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-fifo": { "version": "1.3.2", @@ -11288,7 +11472,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/fastest-levenshtein": { @@ -17008,7 +17191,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "dependencies": { "argparse": "^2.0.1" }, @@ -17102,6 +17284,29 @@ "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", "dev": true }, + "node_modules/json-schema-compare": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/json-schema-compare/-/json-schema-compare-0.2.2.tgz", + "integrity": "sha512-c4WYmDKyJXhs7WWvAWm3uIYnfyWFoIp+JEoX34rctVvEkMYCPGhXtvmFFXiffBbxfZsvQ0RNnV5H7GvDF5HCqQ==", + "peer": true, + "dependencies": { + "lodash": "^4.17.4" + } + }, + "node_modules/json-schema-merge-allof": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/json-schema-merge-allof/-/json-schema-merge-allof-0.8.1.tgz", + "integrity": "sha512-CTUKmIlPJbsWfzRRnOXz+0MjIqvnleIXwFTzz+t9T86HnYX/Rozria6ZVGLktAU9e+NygNljveP+yxqtQp/Q4w==", + "peer": true, + "dependencies": { + "compute-lcm": "^1.1.2", + "json-schema-compare": "^0.2.2", + "lodash": "^4.17.20" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -17158,6 +17363,15 @@ "node": ">= 10.0.0" } }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/jsprim": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", @@ -17634,6 +17848,17 @@ "tmpl": "1.0.5" } }, + "node_modules/markdown-to-jsx": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.7.4.tgz", + "integrity": "sha512-1bSfXyBKi+EYS3YY+e0Csuxf8oZ3decdfhOav/Z7Wrk89tjudyL5FOmwZQUoy0/qVXGUl+6Q3s2SWtpDEWITfQ==", + "engines": { + "node": ">= 10" + }, + "peerDependencies": { + "react": ">= 0.14.0" + } + }, "node_modules/marked": { "version": "4.2.12", "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.12.tgz", @@ -17995,6 +18220,12 @@ "url": "https://opencollective.com/mobx" } }, + "node_modules/monaco-editor": { + "version": "0.52.2", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz", + "integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==", + "peer": true + }, "node_modules/moo": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.1.tgz", @@ -19574,7 +19805,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true, "engines": { "node": ">=6" } @@ -20241,7 +20471,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -21471,6 +21700,11 @@ "node": ">=8" } }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==" + }, "node_modules/statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", @@ -22930,7 +23164,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "dependencies": { "punycode": "^2.1.0" } @@ -23019,6 +23252,43 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/validate.io-array": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/validate.io-array/-/validate.io-array-1.0.6.tgz", + "integrity": "sha512-DeOy7CnPEziggrOO5CZhVKJw6S3Yi7e9e65R1Nl/RTN1vTQKnzjfvks0/8kQ40FP/dsjRAOd4hxmJ7uLa6vxkg==", + "peer": true + }, + "node_modules/validate.io-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/validate.io-function/-/validate.io-function-1.0.2.tgz", + "integrity": "sha512-LlFybRJEriSuBnUhQyG5bwglhh50EpTL2ul23MPIuR1odjO7XaMLFV8vHGwp7AZciFxtYOeiSCT5st+XSPONiQ==", + "peer": true + }, + "node_modules/validate.io-integer": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/validate.io-integer/-/validate.io-integer-1.0.5.tgz", + "integrity": "sha512-22izsYSLojN/P6bppBqhgUDjCkr5RY2jd+N2a3DCAUey8ydvrZ/OkGvFPR7qfOpwR2LC5p4Ngzxz36g5Vgr/hQ==", + "peer": true, + "dependencies": { + "validate.io-number": "^1.0.3" + } + }, + "node_modules/validate.io-integer-array": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/validate.io-integer-array/-/validate.io-integer-array-1.0.0.tgz", + "integrity": "sha512-mTrMk/1ytQHtCY0oNO3dztafHYyGU88KL+jRxWuzfOmQb+4qqnWmI+gykvGp8usKZOM0H7keJHEbRaFiYA0VrA==", + "peer": true, + "dependencies": { + "validate.io-array": "^1.0.3", + "validate.io-integer": "^1.0.4" + } + }, + "node_modules/validate.io-number": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/validate.io-number/-/validate.io-number-1.0.3.tgz", + "integrity": "sha512-kRAyotcbNaSYoDnXvb4MHg/0a1egJdLwS6oJ38TJY7aw9n93Fl/3blIXdyYvPOp55CNxywooG/3BcrwNrBpcSg==", + "peer": true + }, "node_modules/value-equal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", @@ -27311,6 +27581,22 @@ "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", "dev": true }, + "@monaco-editor/loader": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.5.0.tgz", + "integrity": "sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw==", + "requires": { + "state-local": "^1.0.6" + } + }, + "@monaco-editor/react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", + "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", + "requires": { + "@monaco-editor/loader": "^1.5.0" + } + }, "@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -27490,10 +27776,43 @@ "victory-zoom-container": "^37.1.1" } }, + "@patternfly/react-code-editor": { + "version": "5.4.18", + "resolved": "https://registry.npmjs.org/@patternfly/react-code-editor/-/react-code-editor-5.4.18.tgz", + "integrity": "sha512-xFtdRj9NwHhi1pfIQgj2hrvDqjMi3pLiRKhdI+TcUjnRJQygjZdEvklsEHHoZmUELsNf/98yrwsWt0iLOzdvdQ==", + "requires": { + "@monaco-editor/react": "^4.6.0", + "@patternfly/react-core": "^5.4.14", + "@patternfly/react-icons": "^5.4.2", + "@patternfly/react-styles": "^5.4.1", + "react-dropzone": "14.2.3", + "tslib": "^2.7.0" + }, + "dependencies": { + "file-selector": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz", + "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==", + "requires": { + "tslib": "^2.4.0" + } + }, + "react-dropzone": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz", + "integrity": "sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==", + "requires": { + "attr-accept": "^2.2.2", + "file-selector": "^0.6.0", + "prop-types": "^15.8.1" + } + } + } + }, "@patternfly/react-core": { - "version": "5.4.10", - "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-5.4.10.tgz", - "integrity": "sha512-lThdXKqHc9sN5AqPskZa1YmUmSwbjdkaT8neTCdMjfnBaBanYCmSCNUMYUsoB0L/2siu2Vlcp12RRaG5hZeN2g==", + "version": "5.4.14", + "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-5.4.14.tgz", + "integrity": "sha512-oXVMzLs9Pa+xmdc39L2u05zbXfY3mWuOFi4GDv44GPdDexZUFy5W69+Nv5P8cwfMim55Nf5kKYpcqmatD2bBXw==", "requires": { "@patternfly/react-icons": "^5.4.2", "@patternfly/react-styles": "^5.4.1", @@ -27589,6 +27908,68 @@ "integrity": "sha512-xfSkCAchbdG5PnbrKqFWwia4Bi61nH+wm8wLEqfHDyp7Y3dZzgqS2itV8i4gAq9pC2HsTpwyBC6Ds8VHZ96JlA==", "dev": true }, + "@rjsf/core": { + "version": "5.24.12", + "resolved": "https://registry.npmjs.org/@rjsf/core/-/core-5.24.12.tgz", + "integrity": "sha512-OWVdC501n3Io0hplgpnkzArpcUSiImMgLQhk6/EI8wu2xbvk5fTiM7YAVlAObpAD3z3LRrAwhjnmh9L4k/FWmQ==", + "requires": { + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "markdown-to-jsx": "^7.4.1", + "nanoid": "^3.3.7", + "prop-types": "^15.8.1" + } + }, + "@rjsf/utils": { + "version": "5.24.12", + "resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-5.24.12.tgz", + "integrity": "sha512-fDwQB0XkjZjpdFUz5UAnuZj8nnbxDbX5tp+jTOjjJKw2TMQ9gFFYCQ12lSpdhezA2YgEGZfxyYTGW0DKDL5Drg==", + "peer": true, + "requires": { + "json-schema-merge-allof": "^0.8.1", + "jsonpointer": "^5.0.1", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "react-is": "^18.2.0" + }, + "dependencies": { + "react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "peer": true + } + } + }, + "@rjsf/validator-ajv8": { + "version": "5.24.12", + "resolved": "https://registry.npmjs.org/@rjsf/validator-ajv8/-/validator-ajv8-5.24.12.tgz", + "integrity": "sha512-IMXdCjvDNdvb+mDgZC3AlAtr0pjYKq5s0GcLECjG5PuiX7Ib4JaDQHZY5ZJdKblMfgzhsn8AAOi573jXAt7BHQ==", + "requires": { + "ajv": "^8.12.0", + "ajv-formats": "^2.1.1", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21" + }, + "dependencies": { + "ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + } + } + }, "@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -28232,6 +28613,12 @@ } } }, + "@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true + }, "@types/jsdom": { "version": "16.2.13", "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-16.2.13.tgz", @@ -28250,9 +28637,9 @@ "dev": true }, "@types/lodash": { - "version": "4.14.178", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.178.tgz", - "integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==", + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", "dev": true }, "@types/mime": { @@ -29057,7 +29444,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, "requires": { "ajv": "^8.0.0" }, @@ -29066,7 +29452,6 @@ "version": "8.10.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.10.0.tgz", "integrity": "sha512-bzqAEZOjkrUMl2afH8dknrq5KEk2SrwdBROR+vH1EKVQTqaUbJVPdc/gEdggTMM0Se+s+Ja4ju4TlNcStKl2Hw==", - "dev": true, "requires": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -29077,8 +29462,7 @@ "json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" } } }, @@ -29164,8 +29548,7 @@ "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "aria-query": { "version": "5.0.0", @@ -30340,6 +30723,29 @@ } } }, + "compute-gcd": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/compute-gcd/-/compute-gcd-1.2.1.tgz", + "integrity": "sha512-TwMbxBNz0l71+8Sc4czv13h4kEqnchV9igQZBi6QUaz09dnz13juGnnaWWJTRsP3brxOoxeB4SA2WELLw1hCtg==", + "peer": true, + "requires": { + "validate.io-array": "^1.0.3", + "validate.io-function": "^1.0.2", + "validate.io-integer-array": "^1.0.0" + } + }, + "compute-lcm": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/compute-lcm/-/compute-lcm-1.1.2.tgz", + "integrity": "sha512-OFNPdQAXnQhDSKioX8/XYT6sdUlXwpeMjfd6ApxMJfyZ4GxmLR1xvMERctlYhlHwIiz6CSpBc2+qYKjHGZw4TQ==", + "peer": true, + "requires": { + "compute-gcd": "^1.2.1", + "validate.io-array": "^1.0.3", + "validate.io-function": "^1.0.2", + "validate.io-integer-array": "^1.0.0" + } + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -32363,8 +32769,7 @@ "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-fifo": { "version": "1.3.2", @@ -32400,8 +32805,7 @@ "fast-uri": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", - "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", - "dev": true + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==" }, "fastest-levenshtein": { "version": "1.0.12", @@ -36434,7 +36838,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "requires": { "argparse": "^2.0.1" } @@ -36505,6 +36908,26 @@ "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", "dev": true }, + "json-schema-compare": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/json-schema-compare/-/json-schema-compare-0.2.2.tgz", + "integrity": "sha512-c4WYmDKyJXhs7WWvAWm3uIYnfyWFoIp+JEoX34rctVvEkMYCPGhXtvmFFXiffBbxfZsvQ0RNnV5H7GvDF5HCqQ==", + "peer": true, + "requires": { + "lodash": "^4.17.4" + } + }, + "json-schema-merge-allof": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/json-schema-merge-allof/-/json-schema-merge-allof-0.8.1.tgz", + "integrity": "sha512-CTUKmIlPJbsWfzRRnOXz+0MjIqvnleIXwFTzz+t9T86HnYX/Rozria6ZVGLktAU9e+NygNljveP+yxqtQp/Q4w==", + "peer": true, + "requires": { + "compute-lcm": "^1.1.2", + "json-schema-compare": "^0.2.2", + "lodash": "^4.17.20" + } + }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -36552,6 +36975,12 @@ } } }, + "jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "peer": true + }, "jsprim": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", @@ -36912,6 +37341,12 @@ "tmpl": "1.0.5" } }, + "markdown-to-jsx": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.7.4.tgz", + "integrity": "sha512-1bSfXyBKi+EYS3YY+e0Csuxf8oZ3decdfhOav/Z7Wrk89tjudyL5FOmwZQUoy0/qVXGUl+6Q3s2SWtpDEWITfQ==", + "requires": {} + }, "marked": { "version": "4.2.12", "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.12.tgz", @@ -37162,6 +37597,12 @@ "resolved": "https://registry.npmjs.org/mobx/-/mobx-6.13.5.tgz", "integrity": "sha512-/HTWzW2s8J1Gqt+WmUj5Y0mddZk+LInejADc79NJadrWla3rHzmRHki/mnEUH1AvOmbNTZ1BRbKxr8DSgfdjMA==" }, + "monaco-editor": { + "version": "0.52.2", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz", + "integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==", + "peer": true + }, "moo": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.1.tgz", @@ -38308,8 +38749,7 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, "pure-rand": { "version": "7.0.1", @@ -38809,8 +39249,7 @@ "require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" }, "require-main-filename": { "version": "1.0.1", @@ -39762,6 +40201,11 @@ } } }, + "state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==" + }, "statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", @@ -40768,7 +41212,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "requires": { "punycode": "^2.1.0" } @@ -40846,6 +41289,43 @@ "spdx-expression-parse": "^3.0.0" } }, + "validate.io-array": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/validate.io-array/-/validate.io-array-1.0.6.tgz", + "integrity": "sha512-DeOy7CnPEziggrOO5CZhVKJw6S3Yi7e9e65R1Nl/RTN1vTQKnzjfvks0/8kQ40FP/dsjRAOd4hxmJ7uLa6vxkg==", + "peer": true + }, + "validate.io-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/validate.io-function/-/validate.io-function-1.0.2.tgz", + "integrity": "sha512-LlFybRJEriSuBnUhQyG5bwglhh50EpTL2ul23MPIuR1odjO7XaMLFV8vHGwp7AZciFxtYOeiSCT5st+XSPONiQ==", + "peer": true + }, + "validate.io-integer": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/validate.io-integer/-/validate.io-integer-1.0.5.tgz", + "integrity": "sha512-22izsYSLojN/P6bppBqhgUDjCkr5RY2jd+N2a3DCAUey8ydvrZ/OkGvFPR7qfOpwR2LC5p4Ngzxz36g5Vgr/hQ==", + "peer": true, + "requires": { + "validate.io-number": "^1.0.3" + } + }, + "validate.io-integer-array": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/validate.io-integer-array/-/validate.io-integer-array-1.0.0.tgz", + "integrity": "sha512-mTrMk/1ytQHtCY0oNO3dztafHYyGU88KL+jRxWuzfOmQb+4qqnWmI+gykvGp8usKZOM0H7keJHEbRaFiYA0VrA==", + "peer": true, + "requires": { + "validate.io-array": "^1.0.3", + "validate.io-integer": "^1.0.4" + } + }, + "validate.io-number": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/validate.io-number/-/validate.io-number-1.0.3.tgz", + "integrity": "sha512-kRAyotcbNaSYoDnXvb4MHg/0a1egJdLwS6oJ38TJY7aw9n93Fl/3blIXdyYvPOp55CNxywooG/3BcrwNrBpcSg==", + "peer": true + }, "value-equal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", diff --git a/web/package.json b/web/package.json index bcd6ee9c4..779401555 100644 --- a/web/package.json +++ b/web/package.json @@ -4,7 +4,9 @@ "private": true, "scripts": { "clean": "rm -rf ./dist", + "clean:static": "rm -rf ./dist/static", "build": "npm run clean && NODE_ENV=production npm run ts-node ./node_modules/.bin/webpack", + "build:static": "npm run clean:static && FLAVOR=static NODE_ENV=production npm run ts-node ./node_modules/.bin/webpack", "build:dev": "npm run clean && npm run ts-node ./node_modules/.bin/webpack", "build:standalone": "npm run clean && webpack --config webpack.standalone.js", "start": "npm run ts-node-transpile ./node_modules/.bin/webpack serve", @@ -38,8 +40,9 @@ "@types/copy-webpack-plugin": "8.0.1", "@types/enzyme": "3.10.x", "@types/jest": "30.0.0", + "@types/js-yaml": "^4.0.9", "@types/jsdom": "^16.2.13", - "@types/lodash": "^4.14.178", + "@types/lodash": "^4.17.20", "@types/murmurhash-js": "^1.0.3", "@types/port-numbers": "^5.0.0", "@types/react": "^17.0.1", @@ -89,15 +92,6 @@ "webpack-node-externals": "3.0.0" }, "consolePlugin": { - "name": "netobserv-plugin", - "version": "0.1.0", - "displayName": "NetObserv Plugin for Console", - "description": "This plugin adds network observability functionality to Openshift console", - "exposedModules": { - "netflowParent": "./components/netflow-traffic-parent.tsx", - "netflowTab": "./components/netflow-traffic-tab.tsx", - "netflowDevTab": "./components/netflow-traffic-dev-tab.tsx" - }, "dependencies": { "@console/pluginAPI": "*" } @@ -106,16 +100,20 @@ "@openshift-console/plugin-shared": "^0.0.3", "@patternfly/patternfly": "^5.4.0", "@patternfly/react-charts": "^7.4.0", + "@patternfly/react-code-editor": "^5.4.0", "@patternfly/react-core": "^5.4.0", "@patternfly/react-icons": "^5.4.0", "@patternfly/react-styles": "^5.4.0", "@patternfly/react-table": "^5.4.0", "@patternfly/react-tokens": "^5.4.0", "@patternfly/react-topology": "^5.4.0", + "@rjsf/core": "^5.24.12", + "@rjsf/validator-ajv8": "^5.24.12", "axios": "^1.8.2", "html-to-image": "^1.11.11", "i18next": "^21.8.14", "i18next-http-backend": "^1.0.21", + "js-yaml": "^4.1.0", "murmurhash-js": "^1.0.0", "percentile": "^1.6.0", "port-numbers": "^6.0.1", diff --git a/web/src/app.tsx b/web/src/app.tsx index 001e8f9b2..93d65d1da 100755 --- a/web/src/app.tsx +++ b/web/src/app.tsx @@ -26,6 +26,13 @@ import { import { BarsIcon } from '@patternfly/react-icons'; import React from 'react'; import { BrowserRouter, Link } from 'react-router-dom'; +import { GetFlowCollectorJS } from './components/forms/config/templates'; +import Consumption from './components/forms/consumption'; +import FlowCollectorForm from './components/forms/flowCollector'; +import FlowCollectorStatus from './components/forms/flowCollector-status'; +import FlowCollectorWizard from './components/forms/flowCollector-wizard'; +import FlowMetricForm from './components/forms/flowMetric'; +import FlowMetricWizard from './components/forms/flowMetric-wizard'; import NetflowTrafficDevTab from './components/netflow-traffic-dev-tab'; import NetflowTrafficParent from './components/netflow-traffic-parent'; import NetflowTab from './components/netflow-traffic-tab'; @@ -55,6 +62,30 @@ export const pages = [ { id: 'udn-tab', name: 'UDN tab' + }, + { + id: 'flowCollector-wizard', + name: 'FlowCollector wizard' + }, + { + id: 'flowCollector', + name: 'FlowCollector form' + }, + { + id: 'flowCollector-consumption', + name: 'FlowCollector consumption' + }, + { + id: 'flowCollector-status', + name: 'FlowCollector status' + }, + { + id: 'flowMetric-wizard', + name: 'FlowMetric wizard' + }, + { + id: 'flowMetric', + name: 'FlowMetric form' } ]; @@ -164,6 +195,18 @@ export const App: React.FunctionComponent = () => { ); case 'udn-tab': return ; + case 'flowCollector-wizard': + return ; + case 'flowCollector': + return ; + case 'flowCollector-consumption': + return ; + case 'flowCollector-status': + return ; + case 'flowMetric-wizard': + return ; + case 'flowMetric': + return ; default: return ; } @@ -175,7 +218,18 @@ export const App: React.FunctionComponent = () => { const content = pageContent(page.id); switch (page.id) { case 'netflow-traffic': + case 'flowCollector-wizard': + case 'flowCollector': + case 'flowCollector-status': + case 'flowMetric-wizard': + case 'flowMetric': return <>{content}; + case 'flowCollector-consumption': + return ( + + {content} + + ); default: return ( diff --git a/web/src/components/drawer/netflow-traffic-drawer.tsx b/web/src/components/drawer/netflow-traffic-drawer.tsx index 3d43d50d8..dd90050ca 100644 --- a/web/src/components/drawer/netflow-traffic-drawer.tsx +++ b/web/src/components/drawer/netflow-traffic-drawer.tsx @@ -23,7 +23,7 @@ import { Column, ColumnSizeMap } from '../../utils/columns'; import { isPromError } from '../../utils/errors'; import { OverviewPanel } from '../../utils/overview-panels'; import { TruncateLength } from '../dropdowns/truncate-dropdown'; -import { Error, Size } from '../messages/error'; +import { ErrorComponent, Size } from '../messages/error'; import { ViewId } from '../netflow-traffic'; import FlowsQuerySummary from '../query-summary/flows-query-summary'; import MetricsQuerySummary from '../query-summary/metrics-query-summary'; @@ -236,7 +236,7 @@ export const NetflowTrafficDrawer: React.FC = React.f let content: JSX.Element | null = null; if (props.error) { content = ( - matches that of any node on which\na pod of the set of pods is running', + type: 'object', + required: ['topologyKey'], + properties: { + labelSelector: { + description: + "A label query over a set of resources, in this case pods.\nIf it's null, this PodAffinityTerm matches with no Pods.", + type: 'object', + properties: { + matchExpressions: { + description: + 'matchExpressions is a list of label selector requirements. The requirements are ANDed.', + type: 'array', + items: { + description: + 'A label selector requirement is a selector that contains values, a key, and an operator that\nrelates the key and values.', + type: 'object', + required: ['key', 'operator'], + properties: { + key: { + description: 'key is the label key that the selector applies to.', + type: 'string' + }, + operator: { + description: + "operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist.", + type: 'string' + }, + values: { + description: + 'values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch.', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + matchLabels: { + description: + 'matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is "key", the\noperator is "In", and the values array contains only "value". The requirements are ANDed.', + type: 'object', + additionalProperties: { + type: 'string' + } + } + }, + 'x-kubernetes-map-type': 'atomic' + }, + matchLabelKeys: { + description: + "MatchLabelKeys is a set of pod label keys to select which pods will\nbe taken into consideration. The keys are used to lookup values from the\nincoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)`\nto select the group of existing pods which pods will be taken into consideration\nfor the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming\npod labels will be ignored. The default value is empty.\nThe same key is forbidden to exist in both matchLabelKeys and labelSelector.\nAlso, matchLabelKeys cannot be set when labelSelector isn't set.\nThis is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).", + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + }, + mismatchLabelKeys: { + description: + "MismatchLabelKeys is a set of pod label keys to select which pods will\nbe taken into consideration. The keys are used to lookup values from the\nincoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)`\nto select the group of existing pods which pods will be taken into consideration\nfor the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming\npod labels will be ignored. The default value is empty.\nThe same key is forbidden to exist in both mismatchLabelKeys and labelSelector.\nAlso, mismatchLabelKeys cannot be set when labelSelector isn't set.\nThis is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).", + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + }, + namespaceSelector: { + description: + 'A label query over the set of namespaces that the term applies to.\nThe term is applied to the union of the namespaces selected by this field\nand the ones listed in the namespaces field.\nnull selector and null or empty namespaces list means "this pod\'s namespace".\nAn empty selector ({}) matches all namespaces.', + type: 'object', + properties: { + matchExpressions: { + description: + 'matchExpressions is a list of label selector requirements. The requirements are ANDed.', + type: 'array', + items: { + description: + 'A label selector requirement is a selector that contains values, a key, and an operator that\nrelates the key and values.', + type: 'object', + required: ['key', 'operator'], + properties: { + key: { + description: 'key is the label key that the selector applies to.', + type: 'string' + }, + operator: { + description: + "operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist.", + type: 'string' + }, + values: { + description: + 'values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch.', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + matchLabels: { + description: + 'matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is "key", the\noperator is "In", and the values array contains only "value". The requirements are ANDed.', + type: 'object', + additionalProperties: { + type: 'string' + } + } + }, + 'x-kubernetes-map-type': 'atomic' + }, + namespaces: { + description: + 'namespaces specifies a static list of namespace names that the term applies to.\nThe term is applied to the union of the namespaces listed in this field\nand the ones selected by namespaceSelector.\nnull or empty namespaces list and null namespaceSelector means "this pod\'s namespace".', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + }, + topologyKey: { + description: + 'This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching\nthe labelSelector in the specified namespaces, where co-located is defined as running on a node\nwhose value of the label with key topologyKey matches that of any node on which any of the\nselected pods is running.\nEmpty topologyKey is not allowed.', + type: 'string' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + podAntiAffinity: { + description: + 'Describes pod anti-affinity scheduling rules (e.g. avoid putting this pod in the same node, zone, etc. as some other pod(s)).', + type: 'object', + properties: { + preferredDuringSchedulingIgnoredDuringExecution: { + description: + 'The scheduler will prefer to schedule pods to nodes that satisfy\nthe anti-affinity expressions specified by this field, but it may choose\na node that violates one or more of the expressions. The node that is\nmost preferred is the one with the greatest sum of weights, i.e.\nfor each node that meets all of the scheduling requirements (resource\nrequest, requiredDuringScheduling anti-affinity expressions, etc.),\ncompute a sum by iterating through the elements of this field and adding\n"weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the\nnode(s) with the highest sum are the most preferred.', + type: 'array', + items: { + description: + 'The weights of all of the matched WeightedPodAffinityTerm fields are added per-node to find the most preferred node(s)', + type: 'object', + required: ['podAffinityTerm', 'weight'], + properties: { + podAffinityTerm: { + description: + 'Required. A pod affinity term, associated with the corresponding weight.', + type: 'object', + required: ['topologyKey'], + properties: { + labelSelector: { + description: + "A label query over a set of resources, in this case pods.\nIf it's null, this PodAffinityTerm matches with no Pods.", + type: 'object', + properties: { + matchExpressions: { + description: + 'matchExpressions is a list of label selector requirements. The requirements are ANDed.', + type: 'array', + items: { + description: + 'A label selector requirement is a selector that contains values, a key, and an operator that\nrelates the key and values.', + type: 'object', + required: ['key', 'operator'], + properties: { + key: { + description: 'key is the label key that the selector applies to.', + type: 'string' + }, + operator: { + description: + "operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist.", + type: 'string' + }, + values: { + description: + 'values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch.', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + matchLabels: { + description: + 'matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is "key", the\noperator is "In", and the values array contains only "value". The requirements are ANDed.', + type: 'object', + additionalProperties: { + type: 'string' + } + } + }, + 'x-kubernetes-map-type': 'atomic' + }, + matchLabelKeys: { + description: + "MatchLabelKeys is a set of pod label keys to select which pods will\nbe taken into consideration. The keys are used to lookup values from the\nincoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)`\nto select the group of existing pods which pods will be taken into consideration\nfor the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming\npod labels will be ignored. The default value is empty.\nThe same key is forbidden to exist in both matchLabelKeys and labelSelector.\nAlso, matchLabelKeys cannot be set when labelSelector isn't set.\nThis is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).", + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + }, + mismatchLabelKeys: { + description: + "MismatchLabelKeys is a set of pod label keys to select which pods will\nbe taken into consideration. The keys are used to lookup values from the\nincoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)`\nto select the group of existing pods which pods will be taken into consideration\nfor the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming\npod labels will be ignored. The default value is empty.\nThe same key is forbidden to exist in both mismatchLabelKeys and labelSelector.\nAlso, mismatchLabelKeys cannot be set when labelSelector isn't set.\nThis is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).", + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + }, + namespaceSelector: { + description: + 'A label query over the set of namespaces that the term applies to.\nThe term is applied to the union of the namespaces selected by this field\nand the ones listed in the namespaces field.\nnull selector and null or empty namespaces list means "this pod\'s namespace".\nAn empty selector ({}) matches all namespaces.', + type: 'object', + properties: { + matchExpressions: { + description: + 'matchExpressions is a list of label selector requirements. The requirements are ANDed.', + type: 'array', + items: { + description: + 'A label selector requirement is a selector that contains values, a key, and an operator that\nrelates the key and values.', + type: 'object', + required: ['key', 'operator'], + properties: { + key: { + description: 'key is the label key that the selector applies to.', + type: 'string' + }, + operator: { + description: + "operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist.", + type: 'string' + }, + values: { + description: + 'values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch.', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + matchLabels: { + description: + 'matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is "key", the\noperator is "In", and the values array contains only "value". The requirements are ANDed.', + type: 'object', + additionalProperties: { + type: 'string' + } + } + }, + 'x-kubernetes-map-type': 'atomic' + }, + namespaces: { + description: + 'namespaces specifies a static list of namespace names that the term applies to.\nThe term is applied to the union of the namespaces listed in this field\nand the ones selected by namespaceSelector.\nnull or empty namespaces list and null namespaceSelector means "this pod\'s namespace".', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + }, + topologyKey: { + description: + 'This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching\nthe labelSelector in the specified namespaces, where co-located is defined as running on a node\nwhose value of the label with key topologyKey matches that of any node on which any of the\nselected pods is running.\nEmpty topologyKey is not allowed.', + type: 'string' + } + } + }, + weight: { + description: + 'weight associated with matching the corresponding podAffinityTerm,\nin the range 1-100.', + type: 'integer', + format: 'int32' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + requiredDuringSchedulingIgnoredDuringExecution: { + description: + 'If the anti-affinity requirements specified by this field are not met at\nscheduling time, the pod will not be scheduled onto the node.\nIf the anti-affinity requirements specified by this field cease to be met\nat some point during pod execution (e.g. due to a pod label update), the\nsystem may or may not try to eventually evict the pod from its node.\nWhen there are multiple elements, the lists of nodes corresponding to each\npodAffinityTerm are intersected, i.e. all terms must be satisfied.', + type: 'array', + items: { + description: + 'Defines a set of pods (namely those matching the labelSelector\nrelative to the given namespace(s)) that this pod should be\nco-located (affinity) or not co-located (anti-affinity) with,\nwhere co-located is defined as running on a node whose value of\nthe label with key matches that of any node on which\na pod of the set of pods is running', + type: 'object', + required: ['topologyKey'], + properties: { + labelSelector: { + description: + "A label query over a set of resources, in this case pods.\nIf it's null, this PodAffinityTerm matches with no Pods.", + type: 'object', + properties: { + matchExpressions: { + description: + 'matchExpressions is a list of label selector requirements. The requirements are ANDed.', + type: 'array', + items: { + description: + 'A label selector requirement is a selector that contains values, a key, and an operator that\nrelates the key and values.', + type: 'object', + required: ['key', 'operator'], + properties: { + key: { + description: 'key is the label key that the selector applies to.', + type: 'string' + }, + operator: { + description: + "operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist.", + type: 'string' + }, + values: { + description: + 'values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch.', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + matchLabels: { + description: + 'matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is "key", the\noperator is "In", and the values array contains only "value". The requirements are ANDed.', + type: 'object', + additionalProperties: { + type: 'string' + } + } + }, + 'x-kubernetes-map-type': 'atomic' + }, + matchLabelKeys: { + description: + "MatchLabelKeys is a set of pod label keys to select which pods will\nbe taken into consideration. The keys are used to lookup values from the\nincoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)`\nto select the group of existing pods which pods will be taken into consideration\nfor the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming\npod labels will be ignored. The default value is empty.\nThe same key is forbidden to exist in both matchLabelKeys and labelSelector.\nAlso, matchLabelKeys cannot be set when labelSelector isn't set.\nThis is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).", + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + }, + mismatchLabelKeys: { + description: + "MismatchLabelKeys is a set of pod label keys to select which pods will\nbe taken into consideration. The keys are used to lookup values from the\nincoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)`\nto select the group of existing pods which pods will be taken into consideration\nfor the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming\npod labels will be ignored. The default value is empty.\nThe same key is forbidden to exist in both mismatchLabelKeys and labelSelector.\nAlso, mismatchLabelKeys cannot be set when labelSelector isn't set.\nThis is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).", + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + }, + namespaceSelector: { + description: + 'A label query over the set of namespaces that the term applies to.\nThe term is applied to the union of the namespaces selected by this field\nand the ones listed in the namespaces field.\nnull selector and null or empty namespaces list means "this pod\'s namespace".\nAn empty selector ({}) matches all namespaces.', + type: 'object', + properties: { + matchExpressions: { + description: + 'matchExpressions is a list of label selector requirements. The requirements are ANDed.', + type: 'array', + items: { + description: + 'A label selector requirement is a selector that contains values, a key, and an operator that\nrelates the key and values.', + type: 'object', + required: ['key', 'operator'], + properties: { + key: { + description: 'key is the label key that the selector applies to.', + type: 'string' + }, + operator: { + description: + "operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist.", + type: 'string' + }, + values: { + description: + 'values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch.', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + matchLabels: { + description: + 'matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is "key", the\noperator is "In", and the values array contains only "value". The requirements are ANDed.', + type: 'object', + additionalProperties: { + type: 'string' + } + } + }, + 'x-kubernetes-map-type': 'atomic' + }, + namespaces: { + description: + 'namespaces specifies a static list of namespace names that the term applies to.\nThe term is applied to the union of the namespaces listed in this field\nand the ones selected by namespaceSelector.\nnull or empty namespaces list and null namespaceSelector means "this pod\'s namespace".', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + }, + topologyKey: { + description: + 'This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching\nthe labelSelector in the specified namespaces, where co-located is defined as running on a node\nwhose value of the label with key topologyKey matches that of any node on which any of the\nselected pods is running.\nEmpty topologyKey is not allowed.', + type: 'string' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + } + } + } + } + }, + nodeSelector: { + description: + '`nodeSelector` allows scheduling of pods only onto nodes that have each of the specified labels.\nFor documentation, refer to https://kubernetes.io/docs/concepts/configuration/assign-pod-node/.', + type: 'object', + additionalProperties: { + type: 'string' + }, + 'x-kubernetes-map-type': 'atomic' + }, + priorityClassName: { + description: + "If specified, indicates the pod's priority. For documentation, refer to https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/#how-to-use-priority-and-preemption.\nIf not specified, default priority is used, or zero if there is no default.", + type: 'string' + }, + tolerations: { + description: + '`tolerations` is a list of tolerations that allow the pod to schedule onto nodes with matching taints.\nFor documentation, refer to https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#scheduling.', + type: 'array', + items: { + description: + 'The pod this Toleration is attached to tolerates any taint that matches\nthe triple using the matching operator .', + type: 'object', + properties: { + effect: { + description: + 'Effect indicates the taint effect to match. Empty means match all taint effects.\nWhen specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute.', + type: 'string' + }, + key: { + description: + 'Key is the taint key that the toleration applies to. Empty means match all taint keys.\nIf the key is empty, operator must be Exists; this combination means to match all values and all keys.', + type: 'string' + }, + operator: { + description: + "Operator represents a key's relationship to the value.\nValid operators are Exists and Equal. Defaults to Equal.\nExists is equivalent to wildcard for value, so that a pod can\ntolerate all taints of a particular category.", + type: 'string' + }, + tolerationSeconds: { + description: + 'TolerationSeconds represents the period of time the toleration (which must be\nof effect NoExecute, otherwise this field is ignored) tolerates the taint. By default,\nit is not set, which means tolerate the taint forever (do not evict). Zero and\nnegative values will be treated as 0 (evict immediately) by the system.', + type: 'integer', + format: 'int64' + }, + value: { + description: + 'Value is the taint value the toleration matches to.\nIf the operator is Exists, the value should be empty, otherwise just a regular string.', + type: 'string' + } + } + } + } + } + } + } + } + } + }, + ipfix: { + description: + '`ipfix` [deprecated (*)] - describes the settings related to the IPFIX-based flow reporter when `spec.agent.type`\nis set to `IPFIX`.', + type: 'object', + properties: { + cacheActiveTimeout: { + description: + '`cacheActiveTimeout` is the max period during which the reporter aggregates flows before sending.', + type: 'string', + default: '20s', + pattern: '^\\d+(ns|ms|s|m)?$' + }, + cacheMaxFlows: { + description: + '`cacheMaxFlows` is the max number of flows in an aggregate; when reached, the reporter sends the flows.', + type: 'integer', + format: 'int32', + default: 400, + minimum: 0 + }, + clusterNetworkOperator: { + description: + '`clusterNetworkOperator` defines the settings related to the OpenShift Cluster Network Operator, when available.', + type: 'object', + properties: { + namespace: { + description: 'Namespace where the config map is going to be deployed.', + type: 'string', + default: 'openshift-network-operator' + } + } + }, + forceSampleAll: { + description: + '`forceSampleAll` allows disabling sampling in the IPFIX-based flow reporter.\nIt is not recommended to sample all the traffic with IPFIX, as it might generate cluster instability.\nIf you REALLY want to do that, set this flag to `true`. Use at your own risk.\nWhen it is set to `true`, the value of `sampling` is ignored.', + type: 'boolean', + default: false + }, + ovnKubernetes: { + description: + "`ovnKubernetes` defines the settings of the OVN-Kubernetes network plugin, when available. This configuration is used when using OVN's IPFIX exports, without OpenShift. When using OpenShift, refer to the `clusterNetworkOperator` property instead.", + type: 'object', + properties: { + containerName: { + description: '`containerName` defines the name of the container to configure for IPFIX.', + type: 'string', + default: 'ovnkube-node' + }, + daemonSetName: { + description: + '`daemonSetName` defines the name of the DaemonSet controlling the OVN-Kubernetes pods.', + type: 'string', + default: 'ovnkube-node' + }, + namespace: { + description: 'Namespace where OVN-Kubernetes pods are deployed.', + type: 'string', + default: 'ovn-kubernetes' + } + } + }, + sampling: { + description: + '`sampling` is the sampling rate on the reporter. 100 means one flow on 100 is sent.\nTo ensure cluster stability, it is not possible to set a value below 2.\nIf you really want to sample every packet, which might impact the cluster stability,\nrefer to `forceSampleAll`. Alternatively, you can use the eBPF Agent instead of IPFIX.', + type: 'integer', + format: 'int32', + default: 400, + minimum: 2 + } + } + }, + type: { + description: + '`type` [deprecated (*)] selects the flows tracing agent. Previously, this field allowed to select between `eBPF` or `IPFIX`.\nOnly `eBPF` is allowed now, so this field is deprecated and is planned for removal in a future version of the API.', + type: 'string', + default: 'eBPF', + enum: ['eBPF', 'IPFIX'] + } + } + }, + processor: { + description: + '`processor` defines the settings of the component that receives the flows from the agent,\nenriches them, generates metrics, and forwards them to the Loki persistence layer and/or any available exporter.', + type: 'object', + properties: { + logLevel: { + description: '`logLevel` of the processor runtime', + type: 'string', + default: 'info', + enum: ['trace', 'debug', 'info', 'warn', 'error', 'fatal', 'panic'] + }, + advanced: { + description: + '`advanced` allows setting some aspects of the internal configuration of the flow processor.\nThis section is aimed mostly for debugging and fine-grained performance optimizations,\nsuch as `GOGC` and `GOMAXPROCS` env vars. Set these values at your own risk.', + type: 'object', + properties: { + port: { + description: + 'Port of the flow collector (host port).\nBy convention, some values are forbidden. It must be greater than 1024 and different from\n4500, 4789 and 6081.', + type: 'integer', + format: 'int32', + default: 2055, + maximum: 65535, + minimum: 1025 + }, + conversationTerminatingTimeout: { + description: + '`conversationTerminatingTimeout` is the time to wait from detected FIN flag to end a conversation. Only relevant for TCP flows.', + type: 'string', + default: '5s' + }, + conversationEndTimeout: { + description: + '`conversationEndTimeout` is the time to wait after a network flow is received, to consider the conversation ended.\nThis delay is ignored when a FIN packet is collected for TCP flows (see `conversationTerminatingTimeout` instead).', + type: 'string', + default: '10s' + }, + profilePort: { + description: '`profilePort` allows setting up a Go pprof profiler listening to this port', + type: 'integer', + format: 'int32', + default: 6060, + maximum: 65535, + minimum: 0 + }, + env: { + description: + '`env` allows passing custom environment variables to underlying components. Useful for passing\nsome very concrete performance-tuning options, such as `GOGC` and `GOMAXPROCS`, that should not be\npublicly exposed as part of the FlowCollector descriptor, as they are only useful\nin edge debug or support scenarios.', + type: 'object', + additionalProperties: { + type: 'string' + } + }, + enableKubeProbes: { + description: + '`enableKubeProbes` is a flag to enable or disable Kubernetes liveness and readiness probes', + type: 'boolean', + default: true + }, + scheduling: { + description: 'scheduling controls how the pods are scheduled on nodes.', + type: 'object', + properties: { + affinity: { + description: + "If specified, the pod's scheduling constraints. For documentation, refer to https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#scheduling.", + type: 'object', + properties: { + nodeAffinity: { + description: 'Describes node affinity scheduling rules for the pod.', + type: 'object', + properties: { + preferredDuringSchedulingIgnoredDuringExecution: { + description: + 'The scheduler will prefer to schedule pods to nodes that satisfy\nthe affinity expressions specified by this field, but it may choose\na node that violates one or more of the expressions. The node that is\nmost preferred is the one with the greatest sum of weights, i.e.\nfor each node that meets all of the scheduling requirements (resource\nrequest, requiredDuringScheduling affinity expressions, etc.),\ncompute a sum by iterating through the elements of this field and adding\n"weight" to the sum if the node matches the corresponding matchExpressions; the\nnode(s) with the highest sum are the most preferred.', + type: 'array', + items: { + description: + "An empty preferred scheduling term matches all objects with implicit weight 0\n(i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op).", + type: 'object', + required: ['preference', 'weight'], + properties: { + preference: { + description: 'A node selector term, associated with the corresponding weight.', + type: 'object', + properties: { + matchExpressions: { + description: "A list of node selector requirements by node's labels.", + type: 'array', + items: { + description: + 'A node selector requirement is a selector that contains values, a key, and an operator\nthat relates the key and values.', + type: 'object', + required: ['key', 'operator'], + properties: { + key: { + description: 'The label key that the selector applies to.', + type: 'string' + }, + operator: { + description: + "Represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.", + type: 'string' + }, + values: { + description: + 'An array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. If the operator is Gt or Lt, the values\narray must have a single element, which will be interpreted as an integer.\nThis array is replaced during a strategic merge patch.', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + matchFields: { + description: "A list of node selector requirements by node's fields.", + type: 'array', + items: { + description: + 'A node selector requirement is a selector that contains values, a key, and an operator\nthat relates the key and values.', + type: 'object', + required: ['key', 'operator'], + properties: { + key: { + description: 'The label key that the selector applies to.', + type: 'string' + }, + operator: { + description: + "Represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.", + type: 'string' + }, + values: { + description: + 'An array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. If the operator is Gt or Lt, the values\narray must have a single element, which will be interpreted as an integer.\nThis array is replaced during a strategic merge patch.', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + } + }, + 'x-kubernetes-map-type': 'atomic' + }, + weight: { + description: + 'Weight associated with matching the corresponding nodeSelectorTerm, in the range 1-100.', + type: 'integer', + format: 'int32' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + requiredDuringSchedulingIgnoredDuringExecution: { + description: + 'If the affinity requirements specified by this field are not met at\nscheduling time, the pod will not be scheduled onto the node.\nIf the affinity requirements specified by this field cease to be met\nat some point during pod execution (e.g. due to an update), the system\nmay or may not try to eventually evict the pod from its node.', + type: 'object', + required: ['nodeSelectorTerms'], + properties: { + nodeSelectorTerms: { + description: 'Required. A list of node selector terms. The terms are ORed.', + type: 'array', + items: { + description: + 'A null or empty node selector term matches no objects. The requirements of\nthem are ANDed.\nThe TopologySelectorTerm type implements a subset of the NodeSelectorTerm.', + type: 'object', + properties: { + matchExpressions: { + description: "A list of node selector requirements by node's labels.", + type: 'array', + items: { + description: + 'A node selector requirement is a selector that contains values, a key, and an operator\nthat relates the key and values.', + type: 'object', + required: ['key', 'operator'], + properties: { + key: { + description: 'The label key that the selector applies to.', + type: 'string' + }, + operator: { + description: + "Represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.", + type: 'string' + }, + values: { + description: + 'An array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. If the operator is Gt or Lt, the values\narray must have a single element, which will be interpreted as an integer.\nThis array is replaced during a strategic merge patch.', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + matchFields: { + description: "A list of node selector requirements by node's fields.", + type: 'array', + items: { + description: + 'A node selector requirement is a selector that contains values, a key, and an operator\nthat relates the key and values.', + type: 'object', + required: ['key', 'operator'], + properties: { + key: { + description: 'The label key that the selector applies to.', + type: 'string' + }, + operator: { + description: + "Represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.", + type: 'string' + }, + values: { + description: + 'An array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. If the operator is Gt or Lt, the values\narray must have a single element, which will be interpreted as an integer.\nThis array is replaced during a strategic merge patch.', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + } + }, + 'x-kubernetes-map-type': 'atomic' + }, + 'x-kubernetes-list-type': 'atomic' + } + }, + 'x-kubernetes-map-type': 'atomic' + } + } + }, + podAffinity: { + description: + 'Describes pod affinity scheduling rules (e.g. co-locate this pod in the same node, zone, etc. as some other pod(s)).', + type: 'object', + properties: { + preferredDuringSchedulingIgnoredDuringExecution: { + description: + 'The scheduler will prefer to schedule pods to nodes that satisfy\nthe affinity expressions specified by this field, but it may choose\na node that violates one or more of the expressions. The node that is\nmost preferred is the one with the greatest sum of weights, i.e.\nfor each node that meets all of the scheduling requirements (resource\nrequest, requiredDuringScheduling affinity expressions, etc.),\ncompute a sum by iterating through the elements of this field and adding\n"weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the\nnode(s) with the highest sum are the most preferred.', + type: 'array', + items: { + description: + 'The weights of all of the matched WeightedPodAffinityTerm fields are added per-node to find the most preferred node(s)', + type: 'object', + required: ['podAffinityTerm', 'weight'], + properties: { + podAffinityTerm: { + description: + 'Required. A pod affinity term, associated with the corresponding weight.', + type: 'object', + required: ['topologyKey'], + properties: { + labelSelector: { + description: + "A label query over a set of resources, in this case pods.\nIf it's null, this PodAffinityTerm matches with no Pods.", + type: 'object', + properties: { + matchExpressions: { + description: + 'matchExpressions is a list of label selector requirements. The requirements are ANDed.', + type: 'array', + items: { + description: + 'A label selector requirement is a selector that contains values, a key, and an operator that\nrelates the key and values.', + type: 'object', + required: ['key', 'operator'], + properties: { + key: { + description: 'key is the label key that the selector applies to.', + type: 'string' + }, + operator: { + description: + "operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist.", + type: 'string' + }, + values: { + description: + 'values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch.', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + matchLabels: { + description: + 'matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is "key", the\noperator is "In", and the values array contains only "value". The requirements are ANDed.', + type: 'object', + additionalProperties: { + type: 'string' + } + } + }, + 'x-kubernetes-map-type': 'atomic' + }, + matchLabelKeys: { + description: + "MatchLabelKeys is a set of pod label keys to select which pods will\nbe taken into consideration. The keys are used to lookup values from the\nincoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)`\nto select the group of existing pods which pods will be taken into consideration\nfor the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming\npod labels will be ignored. The default value is empty.\nThe same key is forbidden to exist in both matchLabelKeys and labelSelector.\nAlso, matchLabelKeys cannot be set when labelSelector isn't set.\nThis is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).", + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + }, + mismatchLabelKeys: { + description: + "MismatchLabelKeys is a set of pod label keys to select which pods will\nbe taken into consideration. The keys are used to lookup values from the\nincoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)`\nto select the group of existing pods which pods will be taken into consideration\nfor the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming\npod labels will be ignored. The default value is empty.\nThe same key is forbidden to exist in both mismatchLabelKeys and labelSelector.\nAlso, mismatchLabelKeys cannot be set when labelSelector isn't set.\nThis is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).", + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + }, + namespaceSelector: { + description: + 'A label query over the set of namespaces that the term applies to.\nThe term is applied to the union of the namespaces selected by this field\nand the ones listed in the namespaces field.\nnull selector and null or empty namespaces list means "this pod\'s namespace".\nAn empty selector ({}) matches all namespaces.', + type: 'object', + properties: { + matchExpressions: { + description: + 'matchExpressions is a list of label selector requirements. The requirements are ANDed.', + type: 'array', + items: { + description: + 'A label selector requirement is a selector that contains values, a key, and an operator that\nrelates the key and values.', + type: 'object', + required: ['key', 'operator'], + properties: { + key: { + description: 'key is the label key that the selector applies to.', + type: 'string' + }, + operator: { + description: + "operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist.", + type: 'string' + }, + values: { + description: + 'values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch.', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + matchLabels: { + description: + 'matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is "key", the\noperator is "In", and the values array contains only "value". The requirements are ANDed.', + type: 'object', + additionalProperties: { + type: 'string' + } + } + }, + 'x-kubernetes-map-type': 'atomic' + }, + namespaces: { + description: + 'namespaces specifies a static list of namespace names that the term applies to.\nThe term is applied to the union of the namespaces listed in this field\nand the ones selected by namespaceSelector.\nnull or empty namespaces list and null namespaceSelector means "this pod\'s namespace".', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + }, + topologyKey: { + description: + 'This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching\nthe labelSelector in the specified namespaces, where co-located is defined as running on a node\nwhose value of the label with key topologyKey matches that of any node on which any of the\nselected pods is running.\nEmpty topologyKey is not allowed.', + type: 'string' + } + } + }, + weight: { + description: + 'weight associated with matching the corresponding podAffinityTerm,\nin the range 1-100.', + type: 'integer', + format: 'int32' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + requiredDuringSchedulingIgnoredDuringExecution: { + description: + 'If the affinity requirements specified by this field are not met at\nscheduling time, the pod will not be scheduled onto the node.\nIf the affinity requirements specified by this field cease to be met\nat some point during pod execution (e.g. due to a pod label update), the\nsystem may or may not try to eventually evict the pod from its node.\nWhen there are multiple elements, the lists of nodes corresponding to each\npodAffinityTerm are intersected, i.e. all terms must be satisfied.', + type: 'array', + items: { + description: + 'Defines a set of pods (namely those matching the labelSelector\nrelative to the given namespace(s)) that this pod should be\nco-located (affinity) or not co-located (anti-affinity) with,\nwhere co-located is defined as running on a node whose value of\nthe label with key matches that of any node on which\na pod of the set of pods is running', + type: 'object', + required: ['topologyKey'], + properties: { + labelSelector: { + description: + "A label query over a set of resources, in this case pods.\nIf it's null, this PodAffinityTerm matches with no Pods.", + type: 'object', + properties: { + matchExpressions: { + description: + 'matchExpressions is a list of label selector requirements. The requirements are ANDed.', + type: 'array', + items: { + description: + 'A label selector requirement is a selector that contains values, a key, and an operator that\nrelates the key and values.', + type: 'object', + required: ['key', 'operator'], + properties: { + key: { + description: 'key is the label key that the selector applies to.', + type: 'string' + }, + operator: { + description: + "operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist.", + type: 'string' + }, + values: { + description: + 'values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch.', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + matchLabels: { + description: + 'matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is "key", the\noperator is "In", and the values array contains only "value". The requirements are ANDed.', + type: 'object', + additionalProperties: { + type: 'string' + } + } + }, + 'x-kubernetes-map-type': 'atomic' + }, + matchLabelKeys: { + description: + "MatchLabelKeys is a set of pod label keys to select which pods will\nbe taken into consideration. The keys are used to lookup values from the\nincoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)`\nto select the group of existing pods which pods will be taken into consideration\nfor the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming\npod labels will be ignored. The default value is empty.\nThe same key is forbidden to exist in both matchLabelKeys and labelSelector.\nAlso, matchLabelKeys cannot be set when labelSelector isn't set.\nThis is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).", + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + }, + mismatchLabelKeys: { + description: + "MismatchLabelKeys is a set of pod label keys to select which pods will\nbe taken into consideration. The keys are used to lookup values from the\nincoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)`\nto select the group of existing pods which pods will be taken into consideration\nfor the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming\npod labels will be ignored. The default value is empty.\nThe same key is forbidden to exist in both mismatchLabelKeys and labelSelector.\nAlso, mismatchLabelKeys cannot be set when labelSelector isn't set.\nThis is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).", + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + }, + namespaceSelector: { + description: + 'A label query over the set of namespaces that the term applies to.\nThe term is applied to the union of the namespaces selected by this field\nand the ones listed in the namespaces field.\nnull selector and null or empty namespaces list means "this pod\'s namespace".\nAn empty selector ({}) matches all namespaces.', + type: 'object', + properties: { + matchExpressions: { + description: + 'matchExpressions is a list of label selector requirements. The requirements are ANDed.', + type: 'array', + items: { + description: + 'A label selector requirement is a selector that contains values, a key, and an operator that\nrelates the key and values.', + type: 'object', + required: ['key', 'operator'], + properties: { + key: { + description: 'key is the label key that the selector applies to.', + type: 'string' + }, + operator: { + description: + "operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist.", + type: 'string' + }, + values: { + description: + 'values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch.', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + matchLabels: { + description: + 'matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is "key", the\noperator is "In", and the values array contains only "value". The requirements are ANDed.', + type: 'object', + additionalProperties: { + type: 'string' + } + } + }, + 'x-kubernetes-map-type': 'atomic' + }, + namespaces: { + description: + 'namespaces specifies a static list of namespace names that the term applies to.\nThe term is applied to the union of the namespaces listed in this field\nand the ones selected by namespaceSelector.\nnull or empty namespaces list and null namespaceSelector means "this pod\'s namespace".', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + }, + topologyKey: { + description: + 'This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching\nthe labelSelector in the specified namespaces, where co-located is defined as running on a node\nwhose value of the label with key topologyKey matches that of any node on which any of the\nselected pods is running.\nEmpty topologyKey is not allowed.', + type: 'string' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + podAntiAffinity: { + description: + 'Describes pod anti-affinity scheduling rules (e.g. avoid putting this pod in the same node, zone, etc. as some other pod(s)).', + type: 'object', + properties: { + preferredDuringSchedulingIgnoredDuringExecution: { + description: + 'The scheduler will prefer to schedule pods to nodes that satisfy\nthe anti-affinity expressions specified by this field, but it may choose\na node that violates one or more of the expressions. The node that is\nmost preferred is the one with the greatest sum of weights, i.e.\nfor each node that meets all of the scheduling requirements (resource\nrequest, requiredDuringScheduling anti-affinity expressions, etc.),\ncompute a sum by iterating through the elements of this field and adding\n"weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the\nnode(s) with the highest sum are the most preferred.', + type: 'array', + items: { + description: + 'The weights of all of the matched WeightedPodAffinityTerm fields are added per-node to find the most preferred node(s)', + type: 'object', + required: ['podAffinityTerm', 'weight'], + properties: { + podAffinityTerm: { + description: + 'Required. A pod affinity term, associated with the corresponding weight.', + type: 'object', + required: ['topologyKey'], + properties: { + labelSelector: { + description: + "A label query over a set of resources, in this case pods.\nIf it's null, this PodAffinityTerm matches with no Pods.", + type: 'object', + properties: { + matchExpressions: { + description: + 'matchExpressions is a list of label selector requirements. The requirements are ANDed.', + type: 'array', + items: { + description: + 'A label selector requirement is a selector that contains values, a key, and an operator that\nrelates the key and values.', + type: 'object', + required: ['key', 'operator'], + properties: { + key: { + description: 'key is the label key that the selector applies to.', + type: 'string' + }, + operator: { + description: + "operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist.", + type: 'string' + }, + values: { + description: + 'values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch.', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + matchLabels: { + description: + 'matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is "key", the\noperator is "In", and the values array contains only "value". The requirements are ANDed.', + type: 'object', + additionalProperties: { + type: 'string' + } + } + }, + 'x-kubernetes-map-type': 'atomic' + }, + matchLabelKeys: { + description: + "MatchLabelKeys is a set of pod label keys to select which pods will\nbe taken into consideration. The keys are used to lookup values from the\nincoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)`\nto select the group of existing pods which pods will be taken into consideration\nfor the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming\npod labels will be ignored. The default value is empty.\nThe same key is forbidden to exist in both matchLabelKeys and labelSelector.\nAlso, matchLabelKeys cannot be set when labelSelector isn't set.\nThis is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).", + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + }, + mismatchLabelKeys: { + description: + "MismatchLabelKeys is a set of pod label keys to select which pods will\nbe taken into consideration. The keys are used to lookup values from the\nincoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)`\nto select the group of existing pods which pods will be taken into consideration\nfor the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming\npod labels will be ignored. The default value is empty.\nThe same key is forbidden to exist in both mismatchLabelKeys and labelSelector.\nAlso, mismatchLabelKeys cannot be set when labelSelector isn't set.\nThis is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).", + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + }, + namespaceSelector: { + description: + 'A label query over the set of namespaces that the term applies to.\nThe term is applied to the union of the namespaces selected by this field\nand the ones listed in the namespaces field.\nnull selector and null or empty namespaces list means "this pod\'s namespace".\nAn empty selector ({}) matches all namespaces.', + type: 'object', + properties: { + matchExpressions: { + description: + 'matchExpressions is a list of label selector requirements. The requirements are ANDed.', + type: 'array', + items: { + description: + 'A label selector requirement is a selector that contains values, a key, and an operator that\nrelates the key and values.', + type: 'object', + required: ['key', 'operator'], + properties: { + key: { + description: 'key is the label key that the selector applies to.', + type: 'string' + }, + operator: { + description: + "operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist.", + type: 'string' + }, + values: { + description: + 'values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch.', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + matchLabels: { + description: + 'matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is "key", the\noperator is "In", and the values array contains only "value". The requirements are ANDed.', + type: 'object', + additionalProperties: { + type: 'string' + } + } + }, + 'x-kubernetes-map-type': 'atomic' + }, + namespaces: { + description: + 'namespaces specifies a static list of namespace names that the term applies to.\nThe term is applied to the union of the namespaces listed in this field\nand the ones selected by namespaceSelector.\nnull or empty namespaces list and null namespaceSelector means "this pod\'s namespace".', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + }, + topologyKey: { + description: + 'This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching\nthe labelSelector in the specified namespaces, where co-located is defined as running on a node\nwhose value of the label with key topologyKey matches that of any node on which any of the\nselected pods is running.\nEmpty topologyKey is not allowed.', + type: 'string' + } + } + }, + weight: { + description: + 'weight associated with matching the corresponding podAffinityTerm,\nin the range 1-100.', + type: 'integer', + format: 'int32' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + requiredDuringSchedulingIgnoredDuringExecution: { + description: + 'If the anti-affinity requirements specified by this field are not met at\nscheduling time, the pod will not be scheduled onto the node.\nIf the anti-affinity requirements specified by this field cease to be met\nat some point during pod execution (e.g. due to a pod label update), the\nsystem may or may not try to eventually evict the pod from its node.\nWhen there are multiple elements, the lists of nodes corresponding to each\npodAffinityTerm are intersected, i.e. all terms must be satisfied.', + type: 'array', + items: { + description: + 'Defines a set of pods (namely those matching the labelSelector\nrelative to the given namespace(s)) that this pod should be\nco-located (affinity) or not co-located (anti-affinity) with,\nwhere co-located is defined as running on a node whose value of\nthe label with key matches that of any node on which\na pod of the set of pods is running', + type: 'object', + required: ['topologyKey'], + properties: { + labelSelector: { + description: + "A label query over a set of resources, in this case pods.\nIf it's null, this PodAffinityTerm matches with no Pods.", + type: 'object', + properties: { + matchExpressions: { + description: + 'matchExpressions is a list of label selector requirements. The requirements are ANDed.', + type: 'array', + items: { + description: + 'A label selector requirement is a selector that contains values, a key, and an operator that\nrelates the key and values.', + type: 'object', + required: ['key', 'operator'], + properties: { + key: { + description: 'key is the label key that the selector applies to.', + type: 'string' + }, + operator: { + description: + "operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist.", + type: 'string' + }, + values: { + description: + 'values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch.', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + matchLabels: { + description: + 'matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is "key", the\noperator is "In", and the values array contains only "value". The requirements are ANDed.', + type: 'object', + additionalProperties: { + type: 'string' + } + } + }, + 'x-kubernetes-map-type': 'atomic' + }, + matchLabelKeys: { + description: + "MatchLabelKeys is a set of pod label keys to select which pods will\nbe taken into consideration. The keys are used to lookup values from the\nincoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)`\nto select the group of existing pods which pods will be taken into consideration\nfor the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming\npod labels will be ignored. The default value is empty.\nThe same key is forbidden to exist in both matchLabelKeys and labelSelector.\nAlso, matchLabelKeys cannot be set when labelSelector isn't set.\nThis is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).", + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + }, + mismatchLabelKeys: { + description: + "MismatchLabelKeys is a set of pod label keys to select which pods will\nbe taken into consideration. The keys are used to lookup values from the\nincoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)`\nto select the group of existing pods which pods will be taken into consideration\nfor the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming\npod labels will be ignored. The default value is empty.\nThe same key is forbidden to exist in both mismatchLabelKeys and labelSelector.\nAlso, mismatchLabelKeys cannot be set when labelSelector isn't set.\nThis is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).", + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + }, + namespaceSelector: { + description: + 'A label query over the set of namespaces that the term applies to.\nThe term is applied to the union of the namespaces selected by this field\nand the ones listed in the namespaces field.\nnull selector and null or empty namespaces list means "this pod\'s namespace".\nAn empty selector ({}) matches all namespaces.', + type: 'object', + properties: { + matchExpressions: { + description: + 'matchExpressions is a list of label selector requirements. The requirements are ANDed.', + type: 'array', + items: { + description: + 'A label selector requirement is a selector that contains values, a key, and an operator that\nrelates the key and values.', + type: 'object', + required: ['key', 'operator'], + properties: { + key: { + description: 'key is the label key that the selector applies to.', + type: 'string' + }, + operator: { + description: + "operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist.", + type: 'string' + }, + values: { + description: + 'values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch.', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + matchLabels: { + description: + 'matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is "key", the\noperator is "In", and the values array contains only "value". The requirements are ANDed.', + type: 'object', + additionalProperties: { + type: 'string' + } + } + }, + 'x-kubernetes-map-type': 'atomic' + }, + namespaces: { + description: + 'namespaces specifies a static list of namespace names that the term applies to.\nThe term is applied to the union of the namespaces listed in this field\nand the ones selected by namespaceSelector.\nnull or empty namespaces list and null namespaceSelector means "this pod\'s namespace".', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + }, + topologyKey: { + description: + 'This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching\nthe labelSelector in the specified namespaces, where co-located is defined as running on a node\nwhose value of the label with key topologyKey matches that of any node on which any of the\nselected pods is running.\nEmpty topologyKey is not allowed.', + type: 'string' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + } + } + } + } + }, + nodeSelector: { + description: + '`nodeSelector` allows scheduling of pods only onto nodes that have each of the specified labels.\nFor documentation, refer to https://kubernetes.io/docs/concepts/configuration/assign-pod-node/.', + type: 'object', + additionalProperties: { + type: 'string' + }, + 'x-kubernetes-map-type': 'atomic' + }, + priorityClassName: { + description: + "If specified, indicates the pod's priority. For documentation, refer to https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/#how-to-use-priority-and-preemption.\nIf not specified, default priority is used, or zero if there is no default.", + type: 'string' + }, + tolerations: { + description: + '`tolerations` is a list of tolerations that allow the pod to schedule onto nodes with matching taints.\nFor documentation, refer to https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#scheduling.', + type: 'array', + items: { + description: + 'The pod this Toleration is attached to tolerates any taint that matches\nthe triple using the matching operator .', + type: 'object', + properties: { + effect: { + description: + 'Effect indicates the taint effect to match. Empty means match all taint effects.\nWhen specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute.', + type: 'string' + }, + key: { + description: + 'Key is the taint key that the toleration applies to. Empty means match all taint keys.\nIf the key is empty, operator must be Exists; this combination means to match all values and all keys.', + type: 'string' + }, + operator: { + description: + "Operator represents a key's relationship to the value.\nValid operators are Exists and Equal. Defaults to Equal.\nExists is equivalent to wildcard for value, so that a pod can\ntolerate all taints of a particular category.", + type: 'string' + }, + tolerationSeconds: { + description: + 'TolerationSeconds represents the period of time the toleration (which must be\nof effect NoExecute, otherwise this field is ignored) tolerates the taint. By default,\nit is not set, which means tolerate the taint forever (do not evict). Zero and\nnegative values will be treated as 0 (evict immediately) by the system.', + type: 'integer', + format: 'int64' + }, + value: { + description: + 'Value is the taint value the toleration matches to.\nIf the operator is Exists, the value should be empty, otherwise just a regular string.', + type: 'string' + } + } + } + } + } + }, + secondaryNetworks: { + description: + 'Defines secondary networks to be checked for resources identification.\nTo guarantee a correct identification, indexed values must form an unique identifier across the cluster.\nIf the same index is used by several resources, those resources might be incorrectly labeled.', + type: 'array', + items: { + type: 'object', + required: ['index', 'name'], + properties: { + index: { + description: + "`index` is a list of fields to use for indexing the pods. They should form a unique Pod identifier across the cluster.\nCan be any of: `MAC`, `IP`, `Interface`.\nFields absent from the 'k8s.v1.cni.cncf.io/network-status' annotation must not be added to the index.", + type: 'array', + items: { + description: + 'Field to index for secondary network pod identification, can be any of: `MAC`, `IP`, `Interface`.', + type: 'string', + enum: ['MAC', 'IP', 'Interface'] + } + }, + name: { + description: + "`name` should match the network name as visible in the pods annotation 'k8s.v1.cni.cncf.io/network-status'.", + type: 'string' + } + } + } + }, + healthPort: { + description: '`healthPort` is a collector HTTP port in the Pod that exposes the health check API', + type: 'integer', + format: 'int32', + default: 8080, + maximum: 65535, + minimum: 1 + }, + dropUnusedFields: { + description: '`dropUnusedFields` [deprecated (*)] this setting is not used anymore.', + type: 'boolean', + default: true + }, + conversationHeartbeatInterval: { + description: + '`conversationHeartbeatInterval` is the time to wait between "tick" events of a conversation', + type: 'string', + default: '30s' + } + } + }, + metrics: { + description: '`Metrics` define the processor configuration regarding metrics', + type: 'object', + properties: { + disableAlerts: { + description: + '`disableAlerts` is a list of alerts that should be disabled.\nPossible values are:\n`NetObservNoFlows`, which is triggered when no flows are being observed for a certain period.\n`NetObservLokiError`, which is triggered when flows are being dropped due to Loki errors.', + type: 'array', + items: { + description: + 'Name of a processor alert.\nPossible values are:\n- `NetObservNoFlows`, which is triggered when no flows are being observed for a certain period.\n- `NetObservLokiError`, which is triggered when flows are being dropped due to Loki errors.', + type: 'string', + enum: ['NetObservNoFlows', 'NetObservLokiError'] + } + }, + includeList: { + description: + '`includeList` is a list of metric names to specify which ones to generate.\nThe names correspond to the names in Prometheus without the prefix. For example,\n`namespace_egress_packets_total` shows up as `netobserv_namespace_egress_packets_total` in Prometheus.\nNote that the more metrics you add, the bigger is the impact on Prometheus workload resources.\nMetrics enabled by default are:\n`namespace_flows_total`, `node_ingress_bytes_total`, `node_egress_bytes_total`, `workload_ingress_bytes_total`,\n`workload_egress_bytes_total`, `namespace_drop_packets_total` (when `PacketDrop` feature is enabled),\n`namespace_rtt_seconds` (when `FlowRTT` feature is enabled), `namespace_dns_latency_seconds` (when `DNSTracking` feature is enabled),\n`namespace_network_policy_events_total` (when `NetworkEvents` feature is enabled).\nMore information, with full list of available metrics: https://github.com/netobserv/network-observability-operator/blob/main/docs/Metrics.md', + type: 'array', + items: { + description: + 'Metric name. More information in https://github.com/netobserv/network-observability-operator/blob/main/docs/Metrics.md.', + type: 'string', + enum: [ + 'namespace_egress_bytes_total', + 'namespace_egress_packets_total', + 'namespace_ingress_bytes_total', + 'namespace_ingress_packets_total', + 'namespace_flows_total', + 'node_egress_bytes_total', + 'node_egress_packets_total', + 'node_ingress_bytes_total', + 'node_ingress_packets_total', + 'node_flows_total', + 'workload_egress_bytes_total', + 'workload_egress_packets_total', + 'workload_ingress_bytes_total', + 'workload_ingress_packets_total', + 'workload_flows_total', + 'namespace_drop_bytes_total', + 'namespace_drop_packets_total', + 'node_drop_bytes_total', + 'node_drop_packets_total', + 'workload_drop_bytes_total', + 'workload_drop_packets_total', + 'namespace_rtt_seconds', + 'node_rtt_seconds', + 'workload_rtt_seconds', + 'namespace_dns_latency_seconds', + 'node_dns_latency_seconds', + 'workload_dns_latency_seconds', + 'node_network_policy_events_total', + 'namespace_network_policy_events_total', + 'workload_network_policy_events_total' + ] + } + }, + server: { + description: 'Metrics server endpoint configuration for Prometheus scraper', + type: 'object', + properties: { + port: { + description: 'The metrics server HTTP port.', + type: 'integer', + format: 'int32', + maximum: 65535, + minimum: 1 + }, + tls: { + description: 'TLS configuration.', + type: 'object', + required: ['type'], + properties: { + insecureSkipVerify: { + description: + '`insecureSkipVerify` allows skipping client-side verification of the provided certificate.\nIf set to `true`, the `providedCaFile` field is ignored.', + type: 'boolean', + default: false + }, + provided: { + description: 'TLS configuration when `type` is set to `Provided`.', + type: 'object', + properties: { + certFile: { + description: + '`certFile` defines the path to the certificate file name within the config map or secret.', + type: 'string' + }, + certKey: { + description: + '`certKey` defines the path to the certificate private key file name within the config map or secret. Omit when the key is not necessary.', + type: 'string' + }, + name: { + description: 'Name of the config map or secret containing certificates.', + type: 'string' + }, + namespace: { + description: + 'Namespace of the config map or secret containing certificates. If omitted, the default is to use the same namespace as where network observability is deployed.\nIf the namespace is different, the config map or the secret is copied so that it can be mounted as required.', + type: 'string', + default: '' + }, + type: { + description: 'Type for the certificate reference: `configmap` or `secret`.', + type: 'string', + enum: ['configmap', 'secret'] + } + } + }, + providedCaFile: { + description: 'Reference to the CA file when `type` is set to `Provided`.', + type: 'object', + properties: { + file: { + description: 'File name within the config map or secret.', + type: 'string' + }, + name: { + description: 'Name of the config map or secret containing the file.', + type: 'string' + }, + namespace: { + description: + 'Namespace of the config map or secret containing the file. If omitted, the default is to use the same namespace as where network observability is deployed.\nIf the namespace is different, the config map or the secret is copied so that it can be mounted as required.', + type: 'string', + default: '' + }, + type: { + description: 'Type for the file reference: `configmap` or `secret`.', + type: 'string', + enum: ['configmap', 'secret'] + } + } + }, + type: { + description: + 'Select the type of TLS configuration:\n- `Disabled` (default) to not configure TLS for the endpoint.\n- `Provided` to manually provide cert file and a key file. [Unsupported (*)].\n- `Auto` to use OpenShift auto generated certificate using annotations.', + type: 'string', + default: 'Disabled', + enum: ['Disabled', 'Provided', 'Auto'] + } + } + } + } + } + } + }, + resources: { + description: + '`resources` are the compute resources required by this container.\nFor more information, see https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/', + type: 'object', + default: { + limits: { + memory: '800Mi' + }, + requests: { + cpu: '100m', + memory: '100Mi' + } + }, + properties: { + claims: { + description: + 'Claims lists the names of resources, defined in spec.resourceClaims,\nthat are used by this container.\n\nThis is an alpha field and requires enabling the\nDynamicResourceAllocation feature gate.\n\nThis field is immutable. It can only be set for containers.', + type: 'array', + items: { + description: 'ResourceClaim references one entry in PodSpec.ResourceClaims.', + type: 'object', + required: ['name'], + properties: { + name: { + description: + 'Name must match the name of one entry in pod.spec.resourceClaims of\nthe Pod where this field is used. It makes that resource available\ninside a container.', + type: 'string' + }, + request: { + description: + 'Request is the name chosen for a request in the referenced claim.\nIf empty, everything from the claim is made available, otherwise\nonly the result of this request.', + type: 'string' + } + } + }, + 'x-kubernetes-list-map-keys': ['name'], + 'x-kubernetes-list-type': 'map' + }, + limits: { + description: + 'Limits describes the maximum amount of compute resources allowed.\nMore info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/', + type: 'object', + additionalProperties: { + pattern: + '^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$', + anyOf: [ + { + type: 'integer' + }, + { + type: 'string' + } + ], + 'x-kubernetes-int-or-string': true + } + }, + requests: { + description: + 'Requests describes the minimum amount of compute resources required.\nIf Requests is omitted for a container, it defaults to Limits if that is explicitly specified,\notherwise to an implementation-defined value. Requests cannot exceed Limits.\nMore info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/', + type: 'object', + additionalProperties: { + pattern: + '^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$', + anyOf: [ + { + type: 'integer' + }, + { + type: 'string' + } + ], + 'x-kubernetes-int-or-string': true + } + } + } + }, + clusterName: { + description: + '`clusterName` is the name of the cluster to appear in the flows data. This is useful in a multi-cluster context. When using OpenShift, leave empty to make it automatically determined.', + type: 'string', + default: '' + }, + multiClusterDeployment: { + description: + 'Set `multiClusterDeployment` to `true` to enable multi clusters feature. This adds `clusterName` label to flows data', + type: 'boolean', + default: false + }, + deduper: { + description: + '`deduper` allows you to sample or drop flows identified as duplicates, in order to save on resource usage.\n[Unsupported (*)].', + type: 'object', + properties: { + mode: { + description: + 'Set the Processor de-duplication mode. It comes in addition to the Agent-based deduplication because the Agent cannot de-duplicate same flows reported from different nodes.\n- Use `Drop` to drop every flow considered as duplicates, allowing saving more on resource usage but potentially losing some information such as the network interfaces used from peer, or network events.\n- Use `Sample` to randomly keep only one flow on 50, which is the default, among the ones considered as duplicates. This is a compromise between dropping every duplicate or keeping every duplicate. This sampling action comes in addition to the Agent-based sampling. If both Agent and Processor sampling values are `50`, the combined sampling is 1:2500.\n- Use `Disabled` to turn off Processor-based de-duplication.', + type: 'string', + default: 'Disabled', + enum: ['Disabled', 'Drop', 'Sample'] + }, + sampling: { + description: '`sampling` is the sampling rate when deduper `mode` is `Sample`.', + type: 'integer', + format: 'int32', + default: 50, + minimum: 0 + } + } + }, + addZone: { + description: + '`addZone` allows availability zone awareness by labelling flows with their source and destination zones.\nThis feature requires the "topology.kubernetes.io/zone" label to be set on nodes.', + type: 'boolean' + }, + kafkaConsumerQueueCapacity: { + description: + '`kafkaConsumerQueueCapacity` defines the capacity of the internal message queue used in the Kafka consumer client. Ignored when not using Kafka.', + type: 'integer', + default: 1000 + }, + imagePullPolicy: { + description: '`imagePullPolicy` is the Kubernetes pull policy for the image defined above', + type: 'string', + default: 'IfNotPresent', + enum: ['IfNotPresent', 'Always', 'Never'] + }, + kafkaConsumerAutoscaler: { + description: + '`kafkaConsumerAutoscaler` is the spec of a horizontal pod autoscaler to set up for `flowlogs-pipeline-transformer`, which consumes Kafka messages.\nThis setting is ignored when Kafka is disabled.', + type: 'object', + properties: { + maxReplicas: { + description: + '`maxReplicas` is the upper limit for the number of pods that can be set by the autoscaler; cannot be smaller than MinReplicas.', + type: 'integer', + format: 'int32', + default: 3 + }, + metrics: { + description: + 'Metrics used by the pod autoscaler. For documentation, refer to https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/horizontal-pod-autoscaler-v2/', + type: 'array', + items: { + type: 'object', + required: ['type'], + properties: { + containerResource: { + type: 'object', + required: ['container', 'name', 'target'], + properties: { + container: { + type: 'string' + }, + name: { + type: 'string' + }, + target: { + type: 'object', + required: ['type'], + properties: { + averageUtilization: { + type: 'integer', + format: 'int32' + }, + averageValue: { + pattern: + '^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$', + anyOf: [ + { + type: 'integer' + }, + { + type: 'string' + } + ], + 'x-kubernetes-int-or-string': true + }, + type: { + type: 'string' + }, + value: { + pattern: + '^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$', + anyOf: [ + { + type: 'integer' + }, + { + type: 'string' + } + ], + 'x-kubernetes-int-or-string': true + } + } + } + } + }, + external: { + type: 'object', + required: ['metric', 'target'], + properties: { + metric: { + type: 'object', + required: ['name'], + properties: { + name: { + type: 'string' + }, + selector: { + type: 'object', + properties: { + matchExpressions: { + type: 'array', + items: { + type: 'object', + required: ['key', 'operator'], + properties: { + key: { + type: 'string' + }, + operator: { + type: 'string' + }, + values: { + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + matchLabels: { + type: 'object', + additionalProperties: { + type: 'string' + } + } + }, + 'x-kubernetes-map-type': 'atomic' + } + } + }, + target: { + type: 'object', + required: ['type'], + properties: { + averageUtilization: { + type: 'integer', + format: 'int32' + }, + averageValue: { + pattern: + '^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$', + anyOf: [ + { + type: 'integer' + }, + { + type: 'string' + } + ], + 'x-kubernetes-int-or-string': true + }, + type: { + type: 'string' + }, + value: { + pattern: + '^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$', + anyOf: [ + { + type: 'integer' + }, + { + type: 'string' + } + ], + 'x-kubernetes-int-or-string': true + } + } + } + } + }, + object: { + type: 'object', + required: ['describedObject', 'metric', 'target'], + properties: { + describedObject: { + type: 'object', + required: ['kind', 'name'], + properties: { + apiVersion: { + type: 'string' + }, + kind: { + type: 'string' + }, + name: { + type: 'string' + } + } + }, + metric: { + type: 'object', + required: ['name'], + properties: { + name: { + type: 'string' + }, + selector: { + type: 'object', + properties: { + matchExpressions: { + type: 'array', + items: { + type: 'object', + required: ['key', 'operator'], + properties: { + key: { + type: 'string' + }, + operator: { + type: 'string' + }, + values: { + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + matchLabels: { + type: 'object', + additionalProperties: { + type: 'string' + } + } + }, + 'x-kubernetes-map-type': 'atomic' + } + } + }, + target: { + type: 'object', + required: ['type'], + properties: { + averageUtilization: { + type: 'integer', + format: 'int32' + }, + averageValue: { + pattern: + '^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$', + anyOf: [ + { + type: 'integer' + }, + { + type: 'string' + } + ], + 'x-kubernetes-int-or-string': true + }, + type: { + type: 'string' + }, + value: { + pattern: + '^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$', + anyOf: [ + { + type: 'integer' + }, + { + type: 'string' + } + ], + 'x-kubernetes-int-or-string': true + } + } + } + } + }, + pods: { + type: 'object', + required: ['metric', 'target'], + properties: { + metric: { + type: 'object', + required: ['name'], + properties: { + name: { + type: 'string' + }, + selector: { + type: 'object', + properties: { + matchExpressions: { + type: 'array', + items: { + type: 'object', + required: ['key', 'operator'], + properties: { + key: { + type: 'string' + }, + operator: { + type: 'string' + }, + values: { + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + matchLabels: { + type: 'object', + additionalProperties: { + type: 'string' + } + } + }, + 'x-kubernetes-map-type': 'atomic' + } + } + }, + target: { + type: 'object', + required: ['type'], + properties: { + averageUtilization: { + type: 'integer', + format: 'int32' + }, + averageValue: { + pattern: + '^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$', + anyOf: [ + { + type: 'integer' + }, + { + type: 'string' + } + ], + 'x-kubernetes-int-or-string': true + }, + type: { + type: 'string' + }, + value: { + pattern: + '^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$', + anyOf: [ + { + type: 'integer' + }, + { + type: 'string' + } + ], + 'x-kubernetes-int-or-string': true + } + } + } + } + }, + resource: { + type: 'object', + required: ['name', 'target'], + properties: { + name: { + type: 'string' + }, + target: { + type: 'object', + required: ['type'], + properties: { + averageUtilization: { + type: 'integer', + format: 'int32' + }, + averageValue: { + pattern: + '^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$', + anyOf: [ + { + type: 'integer' + }, + { + type: 'string' + } + ], + 'x-kubernetes-int-or-string': true + }, + type: { + type: 'string' + }, + value: { + pattern: + '^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$', + anyOf: [ + { + type: 'integer' + }, + { + type: 'string' + } + ], + 'x-kubernetes-int-or-string': true + } + } + } + } + }, + type: { + type: 'string' + } + } + } + }, + minReplicas: { + description: + '`minReplicas` is the lower limit for the number of replicas to which the autoscaler\ncan scale down. It defaults to 1 pod. minReplicas is allowed to be 0 if the\nalpha feature gate HPAScaleToZero is enabled and at least one Object or External\nmetric is configured. Scaling is active as long as at least one metric value is\navailable.', + type: 'integer', + format: 'int32' + }, + status: { + description: + '`status` describes the desired status regarding deploying an horizontal pod autoscaler.\n- `Disabled` does not deploy an horizontal pod autoscaler.\n- `Enabled` deploys an horizontal pod autoscaler.', + type: 'string', + default: 'Disabled', + enum: ['Disabled', 'Enabled'] + } + } + }, + logTypes: { + description: + '`logTypes` defines the desired record types to generate. Possible values are:\n- `Flows` to export regular network flows. This is the default.\n- `Conversations` to generate events for started conversations, ended conversations as well as periodic "tick" updates.\n- `EndedConversations` to generate only ended conversations events.\n- `All` to generate both network flows and all conversations events. It is not recommended due to the impact on resources footprint.', + type: 'string', + default: 'Flows', + enum: ['Flows', 'Conversations', 'EndedConversations', 'All'] + }, + kafkaConsumerReplicas: { + description: + '`kafkaConsumerReplicas` defines the number of replicas (pods) to start for `flowlogs-pipeline-transformer`, which consumes Kafka messages.\nThis setting is ignored when Kafka is disabled.', + type: 'integer', + format: 'int32', + default: 3, + minimum: 0 + }, + filters: { + description: + '`filters` lets you define custom filters to limit the amount of generated flows.\nThese filters provide more flexibility than the eBPF Agent filters (in `spec.agent.ebpf.flowFilter`), such as allowing to filter by Kubernetes namespace,\nbut with a lesser improvement in performance.\n[Unsupported (*)].', + type: 'array', + items: { + description: + '`FLPFilterSet` defines the desired configuration for FLP-based filtering satisfying all conditions.', + type: 'object', + properties: { + allOf: { + description: '`filters` is a list of matches that must be all satisfied in order to remove a flow.', + type: 'array', + items: { + description: '`FLPSingleFilter` defines the desired configuration for a single FLP-based filter.', + type: 'object', + required: ['field', 'matchType'], + properties: { + field: { + description: + 'Name of the field to filter on.\nRefer to the documentation for the list of available fields: https://github.com/netobserv/network-observability-operator/blob/main/docs/flows-format.adoc.', + type: 'string' + }, + matchType: { + description: 'Type of matching to apply.', + type: 'string', + default: 'Equal', + enum: ['Equal', 'NotEqual', 'Presence', 'Absence', 'MatchRegex', 'NotMatchRegex'] + }, + value: { + description: + 'Value to filter on. When `matchType` is `Equal` or `NotEqual`, you can use field injection with `$(SomeField)` to refer to any other field of the flow.', + type: 'string' + } + } + } + }, + outputTarget: { + description: + 'If specified, these filters only target a single output: `Loki`, `Metrics` or `Exporters`. By default, all outputs are targeted.', + type: 'string', + enum: ['', 'Loki', 'Metrics', 'Exporters'] + }, + sampling: { + description: '`sampling` is an optional sampling rate to apply to this filter.', + type: 'integer', + format: 'int32', + minimum: 0 + } + } + } + }, + subnetLabels: { + description: + '`subnetLabels` allows to define custom labels on subnets and IPs or to enable automatic labelling of recognized subnets in OpenShift, which is used to identify cluster external traffic.\nWhen a subnet matches the source or destination IP of a flow, a corresponding field is added: `SrcSubnetLabel` or `DstSubnetLabel`.', + type: 'object', + properties: { + customLabels: { + description: + '`customLabels` allows to customize subnets and IPs labelling, such as to identify cluster-external workloads or web services.\nIf you enable `openShiftAutoDetect`, `customLabels` can override the detected subnets in case they overlap.', + type: 'array', + items: { + description: + 'SubnetLabel allows to label subnets and IPs, such as to identify cluster-external workloads or web services.', + type: 'object', + required: ['cidrs', 'name'], + properties: { + cidrs: { + description: 'List of CIDRs, such as `["1.2.3.4/32"]`.', + type: 'array', + items: { + type: 'string' + } + }, + name: { + description: 'Label name, used to flag matching flows.', + type: 'string' + } + } + } + }, + openShiftAutoDetect: { + description: + '`openShiftAutoDetect` allows, when set to `true`, to detect automatically the machines, pods and services subnets based on the\nOpenShift install configuration and the Cluster Network Operator configuration. Indirectly, this is a way to accurately detect\nexternal traffic: flows that are not labeled for those subnets are external to the cluster. Enabled by default on OpenShift.', + type: 'boolean' + } + } + }, + kafkaConsumerBatchSize: { + description: + '`kafkaConsumerBatchSize` indicates to the broker the maximum batch size, in bytes, that the consumer accepts. Ignored when not using Kafka. Default: 10MB.', + type: 'integer', + default: 10485760 + } + } + }, + prometheus: { + description: + '`prometheus` defines Prometheus settings, such as querier configuration used to fetch metrics from the Console plugin.', + type: 'object', + properties: { + querier: { + description: 'Prometheus querying configuration, such as client settings, used in the Console plugin.', + type: 'object', + required: ['mode'], + properties: { + enable: { + description: + 'When `enable` is `true`, the Console plugin queries flow metrics from Prometheus instead of Loki whenever possible.\nIt is enbaled by default: set it to `false` to disable this feature.\nThe Console plugin can use either Loki or Prometheus as a data source for metrics (see also `spec.loki`), or both.\nNot all queries are transposable from Loki to Prometheus. Hence, if Loki is disabled, some features of the plugin are disabled as well,\nsuch as getting per-pod information or viewing raw flows.\nIf both Prometheus and Loki are enabled, Prometheus takes precedence and Loki is used as a fallback for queries that Prometheus cannot handle.\nIf they are both disabled, the Console plugin is not deployed.', + type: 'boolean' + }, + manual: { + description: 'Prometheus configuration for `Manual` mode.', + type: 'object', + properties: { + forwardUserToken: { + description: 'Set `true` to forward logged in user token in queries to Prometheus', + type: 'boolean' + }, + tls: { + description: 'TLS client configuration for Prometheus URL.', + type: 'object', + properties: { + caCert: { + description: + '`caCert` defines the reference of the certificate for the Certificate Authority.', + type: 'object', + properties: { + certFile: { + description: + '`certFile` defines the path to the certificate file name within the config map or secret.', + type: 'string' + }, + certKey: { + description: + '`certKey` defines the path to the certificate private key file name within the config map or secret. Omit when the key is not necessary.', + type: 'string' + }, + name: { + description: 'Name of the config map or secret containing certificates.', + type: 'string' + }, + namespace: { + description: + 'Namespace of the config map or secret containing certificates. If omitted, the default is to use the same namespace as where network observability is deployed.\nIf the namespace is different, the config map or the secret is copied so that it can be mounted as required.', + type: 'string', + default: '' + }, + type: { + description: 'Type for the certificate reference: `configmap` or `secret`.', + type: 'string', + enum: ['configmap', 'secret'] + } + } + }, + enable: { + description: 'Enable TLS', + type: 'boolean', + default: false + }, + insecureSkipVerify: { + description: + '`insecureSkipVerify` allows skipping client-side verification of the server certificate.\nIf set to `true`, the `caCert` field is ignored.', + type: 'boolean', + default: false + }, + userCert: { + description: + '`userCert` defines the user certificate reference and is used for mTLS. When you use one-way TLS, you can ignore this property.', + type: 'object', + properties: { + certFile: { + description: + '`certFile` defines the path to the certificate file name within the config map or secret.', + type: 'string' + }, + certKey: { + description: + '`certKey` defines the path to the certificate private key file name within the config map or secret. Omit when the key is not necessary.', + type: 'string' + }, + name: { + description: 'Name of the config map or secret containing certificates.', + type: 'string' + }, + namespace: { + description: + 'Namespace of the config map or secret containing certificates. If omitted, the default is to use the same namespace as where network observability is deployed.\nIf the namespace is different, the config map or the secret is copied so that it can be mounted as required.', + type: 'string', + default: '' + }, + type: { + description: 'Type for the certificate reference: `configmap` or `secret`.', + type: 'string', + enum: ['configmap', 'secret'] + } + } + } + } + }, + url: { + description: + '`url` is the address of an existing Prometheus service to use for querying metrics.', + type: 'string', + default: 'http://prometheus:9090' + } + } + }, + mode: { + description: + '`mode` must be set according to the type of Prometheus installation that stores network observability metrics:\n- Use `Auto` to try configuring automatically. In OpenShift, it uses the Thanos querier from OpenShift Cluster Monitoring\n- Use `Manual` for a manual setup', + type: 'string', + default: 'Auto', + enum: ['Manual', 'Auto'] + }, + timeout: { + description: + '`timeout` is the read timeout for console plugin queries to Prometheus.\nA timeout of zero means no timeout.', + type: 'string', + default: '30s' + } + } + } + } + }, + loki: { + description: '`loki`, the flow store, client settings.', + type: 'object', + required: ['mode'], + properties: { + enable: { + description: + 'Set `enable` to `true` to store flows in Loki.\nThe Console plugin can use either Loki or Prometheus as a data source for metrics (see also `spec.prometheus.querier`), or both.\nNot all queries are transposable from Loki to Prometheus. Hence, if Loki is disabled, some features of the plugin are disabled as well,\nsuch as getting per-pod information or viewing raw flows.\nIf both Prometheus and Loki are enabled, Prometheus takes precedence and Loki is used as a fallback for queries that Prometheus cannot handle.\nIf they are both disabled, the Console plugin is not deployed.', + type: 'boolean', + default: true + }, + mode: { + description: + '`mode` must be set according to the installation mode of Loki:\n- Use `LokiStack` when Loki is managed using the Loki Operator\n- Use `Monolithic` when Loki is installed as a monolithic workload\n- Use `Microservices` when Loki is installed as microservices, but without Loki Operator\n- Use `Manual` if none of the options above match your setup', + type: 'string', + default: 'Monolithic', + enum: ['Manual', 'LokiStack', 'Monolithic', 'Microservices'] + }, + manual: { + description: + 'Loki configuration for `Manual` mode. This is the most flexible configuration.\nIt is ignored for other modes.', + type: 'object', + properties: { + authToken: { + description: + '`authToken` describes the way to get a token to authenticate to Loki.\n- `Disabled` does not send any token with the request.\n- `Forward` forwards the user token for authorization.\n- `Host` [deprecated (*)] - uses the local pod service account to authenticate to Loki.\nWhen using the Loki Operator, this must be set to `Forward`.', + type: 'string', + default: 'Disabled', + enum: ['Disabled', 'Host', 'Forward'] + }, + ingesterUrl: { + description: + '`ingesterUrl` is the address of an existing Loki ingester service to push the flows to. When using the Loki Operator,\nset it to the Loki gateway service with the `network` tenant set in path, for example\nhttps://loki-gateway-http.netobserv.svc:8080/api/logs/v1/network.', + type: 'string', + default: 'http://loki:3100/' + }, + querierUrl: { + description: + '`querierUrl` specifies the address of the Loki querier service.\nWhen using the Loki Operator, set it to the Loki gateway service with the `network` tenant set in path, for example\nhttps://loki-gateway-http.netobserv.svc:8080/api/logs/v1/network.', + type: 'string', + default: 'http://loki:3100/' + }, + statusTls: { + description: 'TLS client configuration for Loki status URL.', + type: 'object', + properties: { + caCert: { + description: '`caCert` defines the reference of the certificate for the Certificate Authority.', + type: 'object', + properties: { + certFile: { + description: + '`certFile` defines the path to the certificate file name within the config map or secret.', + type: 'string' + }, + certKey: { + description: + '`certKey` defines the path to the certificate private key file name within the config map or secret. Omit when the key is not necessary.', + type: 'string' + }, + name: { + description: 'Name of the config map or secret containing certificates.', + type: 'string' + }, + namespace: { + description: + 'Namespace of the config map or secret containing certificates. If omitted, the default is to use the same namespace as where network observability is deployed.\nIf the namespace is different, the config map or the secret is copied so that it can be mounted as required.', + type: 'string', + default: '' + }, + type: { + description: 'Type for the certificate reference: `configmap` or `secret`.', + type: 'string', + enum: ['configmap', 'secret'] + } + } + }, + enable: { + description: 'Enable TLS', + type: 'boolean', + default: false + }, + insecureSkipVerify: { + description: + '`insecureSkipVerify` allows skipping client-side verification of the server certificate.\nIf set to `true`, the `caCert` field is ignored.', + type: 'boolean', + default: false + }, + userCert: { + description: + '`userCert` defines the user certificate reference and is used for mTLS. When you use one-way TLS, you can ignore this property.', + type: 'object', + properties: { + certFile: { + description: + '`certFile` defines the path to the certificate file name within the config map or secret.', + type: 'string' + }, + certKey: { + description: + '`certKey` defines the path to the certificate private key file name within the config map or secret. Omit when the key is not necessary.', + type: 'string' + }, + name: { + description: 'Name of the config map or secret containing certificates.', + type: 'string' + }, + namespace: { + description: + 'Namespace of the config map or secret containing certificates. If omitted, the default is to use the same namespace as where network observability is deployed.\nIf the namespace is different, the config map or the secret is copied so that it can be mounted as required.', + type: 'string', + default: '' + }, + type: { + description: 'Type for the certificate reference: `configmap` or `secret`.', + type: 'string', + enum: ['configmap', 'secret'] + } + } + } + } + }, + statusUrl: { + description: + '`statusUrl` specifies the address of the Loki `/ready`, `/metrics` and `/config` endpoints, in case it is different from the\nLoki querier URL. If empty, the `querierUrl` value is used.\nThis is useful to show error messages and some context in the frontend.\nWhen using the Loki Operator, set it to the Loki HTTP query frontend service, for example\nhttps://loki-query-frontend-http.netobserv.svc:3100/.\n`statusTLS` configuration is used when `statusUrl` is set.', + type: 'string' + }, + tenantID: { + description: + '`tenantID` is the Loki `X-Scope-OrgID` that identifies the tenant for each request.\nWhen using the Loki Operator, set it to `network`, which corresponds to a special tenant mode.', + type: 'string', + default: 'netobserv' + }, + tls: { + description: 'TLS client configuration for Loki URL.', + type: 'object', + properties: { + caCert: { + description: '`caCert` defines the reference of the certificate for the Certificate Authority.', + type: 'object', + properties: { + certFile: { + description: + '`certFile` defines the path to the certificate file name within the config map or secret.', + type: 'string' + }, + certKey: { + description: + '`certKey` defines the path to the certificate private key file name within the config map or secret. Omit when the key is not necessary.', + type: 'string' + }, + name: { + description: 'Name of the config map or secret containing certificates.', + type: 'string' + }, + namespace: { + description: + 'Namespace of the config map or secret containing certificates. If omitted, the default is to use the same namespace as where network observability is deployed.\nIf the namespace is different, the config map or the secret is copied so that it can be mounted as required.', + type: 'string', + default: '' + }, + type: { + description: 'Type for the certificate reference: `configmap` or `secret`.', + type: 'string', + enum: ['configmap', 'secret'] + } + } + }, + enable: { + description: 'Enable TLS', + type: 'boolean', + default: false + }, + insecureSkipVerify: { + description: + '`insecureSkipVerify` allows skipping client-side verification of the server certificate.\nIf set to `true`, the `caCert` field is ignored.', + type: 'boolean', + default: false + }, + userCert: { + description: + '`userCert` defines the user certificate reference and is used for mTLS. When you use one-way TLS, you can ignore this property.', + type: 'object', + properties: { + certFile: { + description: + '`certFile` defines the path to the certificate file name within the config map or secret.', + type: 'string' + }, + certKey: { + description: + '`certKey` defines the path to the certificate private key file name within the config map or secret. Omit when the key is not necessary.', + type: 'string' + }, + name: { + description: 'Name of the config map or secret containing certificates.', + type: 'string' + }, + namespace: { + description: + 'Namespace of the config map or secret containing certificates. If omitted, the default is to use the same namespace as where network observability is deployed.\nIf the namespace is different, the config map or the secret is copied so that it can be mounted as required.', + type: 'string', + default: '' + }, + type: { + description: 'Type for the certificate reference: `configmap` or `secret`.', + type: 'string', + enum: ['configmap', 'secret'] + } + } + } + } + } + } + }, + monolithic: { + description: + 'Loki configuration for `Monolithic` mode.\nUse this option when Loki is installed using the monolithic deployment mode (https://grafana.com/docs/loki/latest/fundamentals/architecture/deployment-modes/#monolithic-mode).\nIt is ignored for other modes.', + type: 'object', + properties: { + tenantID: { + description: + '`tenantID` is the Loki `X-Scope-OrgID` header that identifies the tenant for each request.', + type: 'string', + default: 'netobserv' + }, + tls: { + description: 'TLS client configuration for Loki URL.', + type: 'object', + properties: { + caCert: { + description: '`caCert` defines the reference of the certificate for the Certificate Authority.', + type: 'object', + properties: { + certFile: { + description: + '`certFile` defines the path to the certificate file name within the config map or secret.', + type: 'string' + }, + certKey: { + description: + '`certKey` defines the path to the certificate private key file name within the config map or secret. Omit when the key is not necessary.', + type: 'string' + }, + name: { + description: 'Name of the config map or secret containing certificates.', + type: 'string' + }, + namespace: { + description: + 'Namespace of the config map or secret containing certificates. If omitted, the default is to use the same namespace as where network observability is deployed.\nIf the namespace is different, the config map or the secret is copied so that it can be mounted as required.', + type: 'string', + default: '' + }, + type: { + description: 'Type for the certificate reference: `configmap` or `secret`.', + type: 'string', + enum: ['configmap', 'secret'] + } + } + }, + enable: { + description: 'Enable TLS', + type: 'boolean', + default: false + }, + insecureSkipVerify: { + description: + '`insecureSkipVerify` allows skipping client-side verification of the server certificate.\nIf set to `true`, the `caCert` field is ignored.', + type: 'boolean', + default: false + }, + userCert: { + description: + '`userCert` defines the user certificate reference and is used for mTLS. When you use one-way TLS, you can ignore this property.', + type: 'object', + properties: { + certFile: { + description: + '`certFile` defines the path to the certificate file name within the config map or secret.', + type: 'string' + }, + certKey: { + description: + '`certKey` defines the path to the certificate private key file name within the config map or secret. Omit when the key is not necessary.', + type: 'string' + }, + name: { + description: 'Name of the config map or secret containing certificates.', + type: 'string' + }, + namespace: { + description: + 'Namespace of the config map or secret containing certificates. If omitted, the default is to use the same namespace as where network observability is deployed.\nIf the namespace is different, the config map or the secret is copied so that it can be mounted as required.', + type: 'string', + default: '' + }, + type: { + description: 'Type for the certificate reference: `configmap` or `secret`.', + type: 'string', + enum: ['configmap', 'secret'] + } + } + } + } + }, + url: { + description: + '`url` is the unique address of an existing Loki service that points to both the ingester and the querier.', + type: 'string', + default: 'http://loki:3100/' + } + } + }, + microservices: { + description: + 'Loki configuration for `Microservices` mode.\nUse this option when Loki is installed using the microservices deployment mode (https://grafana.com/docs/loki/latest/fundamentals/architecture/deployment-modes/#microservices-mode).\nIt is ignored for other modes.', + type: 'object', + properties: { + ingesterUrl: { + description: + '`ingesterUrl` is the address of an existing Loki ingester service to push the flows to.', + type: 'string', + default: 'http://loki-distributor:3100/' + }, + querierUrl: { + description: '`querierURL` specifies the address of the Loki querier service.', + type: 'string', + default: 'http://loki-query-frontend:3100/' + }, + tenantID: { + description: + '`tenantID` is the Loki `X-Scope-OrgID` header that identifies the tenant for each request.', + type: 'string', + default: 'netobserv' + }, + tls: { + description: 'TLS client configuration for Loki URL.', + type: 'object', + properties: { + caCert: { + description: '`caCert` defines the reference of the certificate for the Certificate Authority.', + type: 'object', + properties: { + certFile: { + description: + '`certFile` defines the path to the certificate file name within the config map or secret.', + type: 'string' + }, + certKey: { + description: + '`certKey` defines the path to the certificate private key file name within the config map or secret. Omit when the key is not necessary.', + type: 'string' + }, + name: { + description: 'Name of the config map or secret containing certificates.', + type: 'string' + }, + namespace: { + description: + 'Namespace of the config map or secret containing certificates. If omitted, the default is to use the same namespace as where network observability is deployed.\nIf the namespace is different, the config map or the secret is copied so that it can be mounted as required.', + type: 'string', + default: '' + }, + type: { + description: 'Type for the certificate reference: `configmap` or `secret`.', + type: 'string', + enum: ['configmap', 'secret'] + } + } + }, + enable: { + description: 'Enable TLS', + type: 'boolean', + default: false + }, + insecureSkipVerify: { + description: + '`insecureSkipVerify` allows skipping client-side verification of the server certificate.\nIf set to `true`, the `caCert` field is ignored.', + type: 'boolean', + default: false + }, + userCert: { + description: + '`userCert` defines the user certificate reference and is used for mTLS. When you use one-way TLS, you can ignore this property.', + type: 'object', + properties: { + certFile: { + description: + '`certFile` defines the path to the certificate file name within the config map or secret.', + type: 'string' + }, + certKey: { + description: + '`certKey` defines the path to the certificate private key file name within the config map or secret. Omit when the key is not necessary.', + type: 'string' + }, + name: { + description: 'Name of the config map or secret containing certificates.', + type: 'string' + }, + namespace: { + description: + 'Namespace of the config map or secret containing certificates. If omitted, the default is to use the same namespace as where network observability is deployed.\nIf the namespace is different, the config map or the secret is copied so that it can be mounted as required.', + type: 'string', + default: '' + }, + type: { + description: 'Type for the certificate reference: `configmap` or `secret`.', + type: 'string', + enum: ['configmap', 'secret'] + } + } + } + } + } + } + }, + lokiStack: { + description: + 'Loki configuration for `LokiStack` mode. This is useful for an easy Loki Operator configuration.\nIt is ignored for other modes.', + type: 'object', + required: ['name'], + properties: { + name: { + description: 'Name of an existing LokiStack resource to use.', + type: 'string', + default: 'loki' + }, + namespace: { + description: + 'Namespace where this `LokiStack` resource is located. If omitted, it is assumed to be the same as `spec.namespace`.', + type: 'string' + } + } + }, + readTimeout: { + description: + '`readTimeout` is the maximum console plugin loki query total time limit.\nA timeout of zero means no timeout.', + type: 'string', + default: '30s' + }, + writeTimeout: { + description: + '`writeTimeout` is the maximum Loki time connection / request limit.\nA timeout of zero means no timeout.', + type: 'string', + default: '10s' + }, + writeBatchWait: { + description: '`writeBatchWait` is the maximum time to wait before sending a Loki batch.', + type: 'string', + default: '1s' + }, + writeBatchSize: { + description: + '`writeBatchSize` is the maximum batch size (in bytes) of Loki logs to accumulate before sending.', + type: 'integer', + format: 'int64', + default: 10485760, + minimum: 1 + }, + advanced: { + description: + '`advanced` allows setting some aspects of the internal configuration of the Loki clients.\nThis section is aimed mostly for debugging and fine-grained performance optimizations.', + type: 'object', + properties: { + staticLabels: { + description: '`staticLabels` is a map of common labels to set on each flow in Loki storage.', + type: 'object', + default: { + app: 'netobserv-flowcollector' + }, + additionalProperties: { + type: 'string' + } + }, + writeMaxBackoff: { + description: + '`writeMaxBackoff` is the maximum backoff time for Loki client connection between retries.', + type: 'string', + default: '5s' + }, + writeMaxRetries: { + description: '`writeMaxRetries` is the maximum number of retries for Loki client connections.', + type: 'integer', + format: 'int32', + default: 2, + minimum: 0 + }, + writeMinBackoff: { + description: + '`writeMinBackoff` is the initial backoff time for Loki client connection between retries.', + type: 'string', + default: '1s' + } + } + } + } + }, + consolePlugin: { + description: '`consolePlugin` defines the settings related to the OpenShift Console plugin, when available.', + type: 'object', + properties: { + logLevel: { + description: '`logLevel` for the console plugin backend', + type: 'string', + default: 'info', + enum: ['trace', 'debug', 'info', 'warn', 'error', 'fatal', 'panic'] + }, + advanced: { + description: + '`advanced` allows setting some aspects of the internal configuration of the console plugin.\nThis section is aimed mostly for debugging and fine-grained performance optimizations,\nsuch as `GOGC` and `GOMAXPROCS` env vars. Set these values at your own risk.', + type: 'object', + properties: { + args: { + description: + '`args` allows passing custom arguments to underlying components. Useful for overriding\nsome parameters, such as a URL or a configuration path, that should not be\npublicly exposed as part of the FlowCollector descriptor, as they are only useful\nin edge debug or support scenarios.', + type: 'array', + items: { + type: 'string' + } + }, + env: { + description: + '`env` allows passing custom environment variables to underlying components. Useful for passing\nsome very concrete performance-tuning options, such as `GOGC` and `GOMAXPROCS`, that should not be\npublicly exposed as part of the FlowCollector descriptor, as they are only useful\nin edge debug or support scenarios.', + type: 'object', + additionalProperties: { + type: 'string' + } + }, + port: { + description: '`port` is the plugin service port. Do not use 9002, which is reserved for metrics.', + type: 'integer', + format: 'int32', + default: 9001, + maximum: 65535, + minimum: 1 + }, + register: { + description: + '`register` allows, when set to `true`, to automatically register the provided console plugin with the OpenShift Console operator.\nWhen set to `false`, you can still register it manually by editing console.operator.openshift.io/cluster with the following command:\n`oc patch console.operator.openshift.io cluster --type=\'json\' -p \'[{"op": "add", "path": "/spec/plugins/-", "value": "netobserv-plugin"}]\'`', + type: 'boolean', + default: true + }, + scheduling: { + description: '`scheduling` controls how the pods are scheduled on nodes.', + type: 'object', + properties: { + affinity: { + description: + "If specified, the pod's scheduling constraints. For documentation, refer to https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#scheduling.", + type: 'object', + properties: { + nodeAffinity: { + description: 'Describes node affinity scheduling rules for the pod.', + type: 'object', + properties: { + preferredDuringSchedulingIgnoredDuringExecution: { + description: + 'The scheduler will prefer to schedule pods to nodes that satisfy\nthe affinity expressions specified by this field, but it may choose\na node that violates one or more of the expressions. The node that is\nmost preferred is the one with the greatest sum of weights, i.e.\nfor each node that meets all of the scheduling requirements (resource\nrequest, requiredDuringScheduling affinity expressions, etc.),\ncompute a sum by iterating through the elements of this field and adding\n"weight" to the sum if the node matches the corresponding matchExpressions; the\nnode(s) with the highest sum are the most preferred.', + type: 'array', + items: { + description: + "An empty preferred scheduling term matches all objects with implicit weight 0\n(i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op).", + type: 'object', + required: ['preference', 'weight'], + properties: { + preference: { + description: 'A node selector term, associated with the corresponding weight.', + type: 'object', + properties: { + matchExpressions: { + description: "A list of node selector requirements by node's labels.", + type: 'array', + items: { + description: + 'A node selector requirement is a selector that contains values, a key, and an operator\nthat relates the key and values.', + type: 'object', + required: ['key', 'operator'], + properties: { + key: { + description: 'The label key that the selector applies to.', + type: 'string' + }, + operator: { + description: + "Represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.", + type: 'string' + }, + values: { + description: + 'An array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. If the operator is Gt or Lt, the values\narray must have a single element, which will be interpreted as an integer.\nThis array is replaced during a strategic merge patch.', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + matchFields: { + description: "A list of node selector requirements by node's fields.", + type: 'array', + items: { + description: + 'A node selector requirement is a selector that contains values, a key, and an operator\nthat relates the key and values.', + type: 'object', + required: ['key', 'operator'], + properties: { + key: { + description: 'The label key that the selector applies to.', + type: 'string' + }, + operator: { + description: + "Represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.", + type: 'string' + }, + values: { + description: + 'An array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. If the operator is Gt or Lt, the values\narray must have a single element, which will be interpreted as an integer.\nThis array is replaced during a strategic merge patch.', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + } + }, + 'x-kubernetes-map-type': 'atomic' + }, + weight: { + description: + 'Weight associated with matching the corresponding nodeSelectorTerm, in the range 1-100.', + type: 'integer', + format: 'int32' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + requiredDuringSchedulingIgnoredDuringExecution: { + description: + 'If the affinity requirements specified by this field are not met at\nscheduling time, the pod will not be scheduled onto the node.\nIf the affinity requirements specified by this field cease to be met\nat some point during pod execution (e.g. due to an update), the system\nmay or may not try to eventually evict the pod from its node.', + type: 'object', + required: ['nodeSelectorTerms'], + properties: { + nodeSelectorTerms: { + description: 'Required. A list of node selector terms. The terms are ORed.', + type: 'array', + items: { + description: + 'A null or empty node selector term matches no objects. The requirements of\nthem are ANDed.\nThe TopologySelectorTerm type implements a subset of the NodeSelectorTerm.', + type: 'object', + properties: { + matchExpressions: { + description: "A list of node selector requirements by node's labels.", + type: 'array', + items: { + description: + 'A node selector requirement is a selector that contains values, a key, and an operator\nthat relates the key and values.', + type: 'object', + required: ['key', 'operator'], + properties: { + key: { + description: 'The label key that the selector applies to.', + type: 'string' + }, + operator: { + description: + "Represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.", + type: 'string' + }, + values: { + description: + 'An array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. If the operator is Gt or Lt, the values\narray must have a single element, which will be interpreted as an integer.\nThis array is replaced during a strategic merge patch.', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + matchFields: { + description: "A list of node selector requirements by node's fields.", + type: 'array', + items: { + description: + 'A node selector requirement is a selector that contains values, a key, and an operator\nthat relates the key and values.', + type: 'object', + required: ['key', 'operator'], + properties: { + key: { + description: 'The label key that the selector applies to.', + type: 'string' + }, + operator: { + description: + "Represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.", + type: 'string' + }, + values: { + description: + 'An array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. If the operator is Gt or Lt, the values\narray must have a single element, which will be interpreted as an integer.\nThis array is replaced during a strategic merge patch.', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + } + }, + 'x-kubernetes-map-type': 'atomic' + }, + 'x-kubernetes-list-type': 'atomic' + } + }, + 'x-kubernetes-map-type': 'atomic' + } + } + }, + podAffinity: { + description: + 'Describes pod affinity scheduling rules (e.g. co-locate this pod in the same node, zone, etc. as some other pod(s)).', + type: 'object', + properties: { + preferredDuringSchedulingIgnoredDuringExecution: { + description: + 'The scheduler will prefer to schedule pods to nodes that satisfy\nthe affinity expressions specified by this field, but it may choose\na node that violates one or more of the expressions. The node that is\nmost preferred is the one with the greatest sum of weights, i.e.\nfor each node that meets all of the scheduling requirements (resource\nrequest, requiredDuringScheduling affinity expressions, etc.),\ncompute a sum by iterating through the elements of this field and adding\n"weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the\nnode(s) with the highest sum are the most preferred.', + type: 'array', + items: { + description: + 'The weights of all of the matched WeightedPodAffinityTerm fields are added per-node to find the most preferred node(s)', + type: 'object', + required: ['podAffinityTerm', 'weight'], + properties: { + podAffinityTerm: { + description: + 'Required. A pod affinity term, associated with the corresponding weight.', + type: 'object', + required: ['topologyKey'], + properties: { + labelSelector: { + description: + "A label query over a set of resources, in this case pods.\nIf it's null, this PodAffinityTerm matches with no Pods.", + type: 'object', + properties: { + matchExpressions: { + description: + 'matchExpressions is a list of label selector requirements. The requirements are ANDed.', + type: 'array', + items: { + description: + 'A label selector requirement is a selector that contains values, a key, and an operator that\nrelates the key and values.', + type: 'object', + required: ['key', 'operator'], + properties: { + key: { + description: 'key is the label key that the selector applies to.', + type: 'string' + }, + operator: { + description: + "operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist.", + type: 'string' + }, + values: { + description: + 'values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch.', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + matchLabels: { + description: + 'matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is "key", the\noperator is "In", and the values array contains only "value". The requirements are ANDed.', + type: 'object', + additionalProperties: { + type: 'string' + } + } + }, + 'x-kubernetes-map-type': 'atomic' + }, + matchLabelKeys: { + description: + "MatchLabelKeys is a set of pod label keys to select which pods will\nbe taken into consideration. The keys are used to lookup values from the\nincoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)`\nto select the group of existing pods which pods will be taken into consideration\nfor the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming\npod labels will be ignored. The default value is empty.\nThe same key is forbidden to exist in both matchLabelKeys and labelSelector.\nAlso, matchLabelKeys cannot be set when labelSelector isn't set.\nThis is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).", + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + }, + mismatchLabelKeys: { + description: + "MismatchLabelKeys is a set of pod label keys to select which pods will\nbe taken into consideration. The keys are used to lookup values from the\nincoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)`\nto select the group of existing pods which pods will be taken into consideration\nfor the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming\npod labels will be ignored. The default value is empty.\nThe same key is forbidden to exist in both mismatchLabelKeys and labelSelector.\nAlso, mismatchLabelKeys cannot be set when labelSelector isn't set.\nThis is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).", + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + }, + namespaceSelector: { + description: + 'A label query over the set of namespaces that the term applies to.\nThe term is applied to the union of the namespaces selected by this field\nand the ones listed in the namespaces field.\nnull selector and null or empty namespaces list means "this pod\'s namespace".\nAn empty selector ({}) matches all namespaces.', + type: 'object', + properties: { + matchExpressions: { + description: + 'matchExpressions is a list of label selector requirements. The requirements are ANDed.', + type: 'array', + items: { + description: + 'A label selector requirement is a selector that contains values, a key, and an operator that\nrelates the key and values.', + type: 'object', + required: ['key', 'operator'], + properties: { + key: { + description: 'key is the label key that the selector applies to.', + type: 'string' + }, + operator: { + description: + "operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist.", + type: 'string' + }, + values: { + description: + 'values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch.', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + matchLabels: { + description: + 'matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is "key", the\noperator is "In", and the values array contains only "value". The requirements are ANDed.', + type: 'object', + additionalProperties: { + type: 'string' + } + } + }, + 'x-kubernetes-map-type': 'atomic' + }, + namespaces: { + description: + 'namespaces specifies a static list of namespace names that the term applies to.\nThe term is applied to the union of the namespaces listed in this field\nand the ones selected by namespaceSelector.\nnull or empty namespaces list and null namespaceSelector means "this pod\'s namespace".', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + }, + topologyKey: { + description: + 'This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching\nthe labelSelector in the specified namespaces, where co-located is defined as running on a node\nwhose value of the label with key topologyKey matches that of any node on which any of the\nselected pods is running.\nEmpty topologyKey is not allowed.', + type: 'string' + } + } + }, + weight: { + description: + 'weight associated with matching the corresponding podAffinityTerm,\nin the range 1-100.', + type: 'integer', + format: 'int32' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + requiredDuringSchedulingIgnoredDuringExecution: { + description: + 'If the affinity requirements specified by this field are not met at\nscheduling time, the pod will not be scheduled onto the node.\nIf the affinity requirements specified by this field cease to be met\nat some point during pod execution (e.g. due to a pod label update), the\nsystem may or may not try to eventually evict the pod from its node.\nWhen there are multiple elements, the lists of nodes corresponding to each\npodAffinityTerm are intersected, i.e. all terms must be satisfied.', + type: 'array', + items: { + description: + 'Defines a set of pods (namely those matching the labelSelector\nrelative to the given namespace(s)) that this pod should be\nco-located (affinity) or not co-located (anti-affinity) with,\nwhere co-located is defined as running on a node whose value of\nthe label with key matches that of any node on which\na pod of the set of pods is running', + type: 'object', + required: ['topologyKey'], + properties: { + labelSelector: { + description: + "A label query over a set of resources, in this case pods.\nIf it's null, this PodAffinityTerm matches with no Pods.", + type: 'object', + properties: { + matchExpressions: { + description: + 'matchExpressions is a list of label selector requirements. The requirements are ANDed.', + type: 'array', + items: { + description: + 'A label selector requirement is a selector that contains values, a key, and an operator that\nrelates the key and values.', + type: 'object', + required: ['key', 'operator'], + properties: { + key: { + description: 'key is the label key that the selector applies to.', + type: 'string' + }, + operator: { + description: + "operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist.", + type: 'string' + }, + values: { + description: + 'values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch.', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + matchLabels: { + description: + 'matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is "key", the\noperator is "In", and the values array contains only "value". The requirements are ANDed.', + type: 'object', + additionalProperties: { + type: 'string' + } + } + }, + 'x-kubernetes-map-type': 'atomic' + }, + matchLabelKeys: { + description: + "MatchLabelKeys is a set of pod label keys to select which pods will\nbe taken into consideration. The keys are used to lookup values from the\nincoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)`\nto select the group of existing pods which pods will be taken into consideration\nfor the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming\npod labels will be ignored. The default value is empty.\nThe same key is forbidden to exist in both matchLabelKeys and labelSelector.\nAlso, matchLabelKeys cannot be set when labelSelector isn't set.\nThis is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).", + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + }, + mismatchLabelKeys: { + description: + "MismatchLabelKeys is a set of pod label keys to select which pods will\nbe taken into consideration. The keys are used to lookup values from the\nincoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)`\nto select the group of existing pods which pods will be taken into consideration\nfor the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming\npod labels will be ignored. The default value is empty.\nThe same key is forbidden to exist in both mismatchLabelKeys and labelSelector.\nAlso, mismatchLabelKeys cannot be set when labelSelector isn't set.\nThis is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).", + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + }, + namespaceSelector: { + description: + 'A label query over the set of namespaces that the term applies to.\nThe term is applied to the union of the namespaces selected by this field\nand the ones listed in the namespaces field.\nnull selector and null or empty namespaces list means "this pod\'s namespace".\nAn empty selector ({}) matches all namespaces.', + type: 'object', + properties: { + matchExpressions: { + description: + 'matchExpressions is a list of label selector requirements. The requirements are ANDed.', + type: 'array', + items: { + description: + 'A label selector requirement is a selector that contains values, a key, and an operator that\nrelates the key and values.', + type: 'object', + required: ['key', 'operator'], + properties: { + key: { + description: 'key is the label key that the selector applies to.', + type: 'string' + }, + operator: { + description: + "operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist.", + type: 'string' + }, + values: { + description: + 'values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch.', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + matchLabels: { + description: + 'matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is "key", the\noperator is "In", and the values array contains only "value". The requirements are ANDed.', + type: 'object', + additionalProperties: { + type: 'string' + } + } + }, + 'x-kubernetes-map-type': 'atomic' + }, + namespaces: { + description: + 'namespaces specifies a static list of namespace names that the term applies to.\nThe term is applied to the union of the namespaces listed in this field\nand the ones selected by namespaceSelector.\nnull or empty namespaces list and null namespaceSelector means "this pod\'s namespace".', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + }, + topologyKey: { + description: + 'This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching\nthe labelSelector in the specified namespaces, where co-located is defined as running on a node\nwhose value of the label with key topologyKey matches that of any node on which any of the\nselected pods is running.\nEmpty topologyKey is not allowed.', + type: 'string' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + podAntiAffinity: { + description: + 'Describes pod anti-affinity scheduling rules (e.g. avoid putting this pod in the same node, zone, etc. as some other pod(s)).', + type: 'object', + properties: { + preferredDuringSchedulingIgnoredDuringExecution: { + description: + 'The scheduler will prefer to schedule pods to nodes that satisfy\nthe anti-affinity expressions specified by this field, but it may choose\na node that violates one or more of the expressions. The node that is\nmost preferred is the one with the greatest sum of weights, i.e.\nfor each node that meets all of the scheduling requirements (resource\nrequest, requiredDuringScheduling anti-affinity expressions, etc.),\ncompute a sum by iterating through the elements of this field and adding\n"weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the\nnode(s) with the highest sum are the most preferred.', + type: 'array', + items: { + description: + 'The weights of all of the matched WeightedPodAffinityTerm fields are added per-node to find the most preferred node(s)', + type: 'object', + required: ['podAffinityTerm', 'weight'], + properties: { + podAffinityTerm: { + description: + 'Required. A pod affinity term, associated with the corresponding weight.', + type: 'object', + required: ['topologyKey'], + properties: { + labelSelector: { + description: + "A label query over a set of resources, in this case pods.\nIf it's null, this PodAffinityTerm matches with no Pods.", + type: 'object', + properties: { + matchExpressions: { + description: + 'matchExpressions is a list of label selector requirements. The requirements are ANDed.', + type: 'array', + items: { + description: + 'A label selector requirement is a selector that contains values, a key, and an operator that\nrelates the key and values.', + type: 'object', + required: ['key', 'operator'], + properties: { + key: { + description: 'key is the label key that the selector applies to.', + type: 'string' + }, + operator: { + description: + "operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist.", + type: 'string' + }, + values: { + description: + 'values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch.', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + matchLabels: { + description: + 'matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is "key", the\noperator is "In", and the values array contains only "value". The requirements are ANDed.', + type: 'object', + additionalProperties: { + type: 'string' + } + } + }, + 'x-kubernetes-map-type': 'atomic' + }, + matchLabelKeys: { + description: + "MatchLabelKeys is a set of pod label keys to select which pods will\nbe taken into consideration. The keys are used to lookup values from the\nincoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)`\nto select the group of existing pods which pods will be taken into consideration\nfor the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming\npod labels will be ignored. The default value is empty.\nThe same key is forbidden to exist in both matchLabelKeys and labelSelector.\nAlso, matchLabelKeys cannot be set when labelSelector isn't set.\nThis is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).", + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + }, + mismatchLabelKeys: { + description: + "MismatchLabelKeys is a set of pod label keys to select which pods will\nbe taken into consideration. The keys are used to lookup values from the\nincoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)`\nto select the group of existing pods which pods will be taken into consideration\nfor the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming\npod labels will be ignored. The default value is empty.\nThe same key is forbidden to exist in both mismatchLabelKeys and labelSelector.\nAlso, mismatchLabelKeys cannot be set when labelSelector isn't set.\nThis is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).", + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + }, + namespaceSelector: { + description: + 'A label query over the set of namespaces that the term applies to.\nThe term is applied to the union of the namespaces selected by this field\nand the ones listed in the namespaces field.\nnull selector and null or empty namespaces list means "this pod\'s namespace".\nAn empty selector ({}) matches all namespaces.', + type: 'object', + properties: { + matchExpressions: { + description: + 'matchExpressions is a list of label selector requirements. The requirements are ANDed.', + type: 'array', + items: { + description: + 'A label selector requirement is a selector that contains values, a key, and an operator that\nrelates the key and values.', + type: 'object', + required: ['key', 'operator'], + properties: { + key: { + description: 'key is the label key that the selector applies to.', + type: 'string' + }, + operator: { + description: + "operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist.", + type: 'string' + }, + values: { + description: + 'values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch.', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + matchLabels: { + description: + 'matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is "key", the\noperator is "In", and the values array contains only "value". The requirements are ANDed.', + type: 'object', + additionalProperties: { + type: 'string' + } + } + }, + 'x-kubernetes-map-type': 'atomic' + }, + namespaces: { + description: + 'namespaces specifies a static list of namespace names that the term applies to.\nThe term is applied to the union of the namespaces listed in this field\nand the ones selected by namespaceSelector.\nnull or empty namespaces list and null namespaceSelector means "this pod\'s namespace".', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + }, + topologyKey: { + description: + 'This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching\nthe labelSelector in the specified namespaces, where co-located is defined as running on a node\nwhose value of the label with key topologyKey matches that of any node on which any of the\nselected pods is running.\nEmpty topologyKey is not allowed.', + type: 'string' + } + } + }, + weight: { + description: + 'weight associated with matching the corresponding podAffinityTerm,\nin the range 1-100.', + type: 'integer', + format: 'int32' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + requiredDuringSchedulingIgnoredDuringExecution: { + description: + 'If the anti-affinity requirements specified by this field are not met at\nscheduling time, the pod will not be scheduled onto the node.\nIf the anti-affinity requirements specified by this field cease to be met\nat some point during pod execution (e.g. due to a pod label update), the\nsystem may or may not try to eventually evict the pod from its node.\nWhen there are multiple elements, the lists of nodes corresponding to each\npodAffinityTerm are intersected, i.e. all terms must be satisfied.', + type: 'array', + items: { + description: + 'Defines a set of pods (namely those matching the labelSelector\nrelative to the given namespace(s)) that this pod should be\nco-located (affinity) or not co-located (anti-affinity) with,\nwhere co-located is defined as running on a node whose value of\nthe label with key matches that of any node on which\na pod of the set of pods is running', + type: 'object', + required: ['topologyKey'], + properties: { + labelSelector: { + description: + "A label query over a set of resources, in this case pods.\nIf it's null, this PodAffinityTerm matches with no Pods.", + type: 'object', + properties: { + matchExpressions: { + description: + 'matchExpressions is a list of label selector requirements. The requirements are ANDed.', + type: 'array', + items: { + description: + 'A label selector requirement is a selector that contains values, a key, and an operator that\nrelates the key and values.', + type: 'object', + required: ['key', 'operator'], + properties: { + key: { + description: 'key is the label key that the selector applies to.', + type: 'string' + }, + operator: { + description: + "operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist.", + type: 'string' + }, + values: { + description: + 'values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch.', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + matchLabels: { + description: + 'matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is "key", the\noperator is "In", and the values array contains only "value". The requirements are ANDed.', + type: 'object', + additionalProperties: { + type: 'string' + } + } + }, + 'x-kubernetes-map-type': 'atomic' + }, + matchLabelKeys: { + description: + "MatchLabelKeys is a set of pod label keys to select which pods will\nbe taken into consideration. The keys are used to lookup values from the\nincoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)`\nto select the group of existing pods which pods will be taken into consideration\nfor the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming\npod labels will be ignored. The default value is empty.\nThe same key is forbidden to exist in both matchLabelKeys and labelSelector.\nAlso, matchLabelKeys cannot be set when labelSelector isn't set.\nThis is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).", + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + }, + mismatchLabelKeys: { + description: + "MismatchLabelKeys is a set of pod label keys to select which pods will\nbe taken into consideration. The keys are used to lookup values from the\nincoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)`\nto select the group of existing pods which pods will be taken into consideration\nfor the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming\npod labels will be ignored. The default value is empty.\nThe same key is forbidden to exist in both mismatchLabelKeys and labelSelector.\nAlso, mismatchLabelKeys cannot be set when labelSelector isn't set.\nThis is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).", + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + }, + namespaceSelector: { + description: + 'A label query over the set of namespaces that the term applies to.\nThe term is applied to the union of the namespaces selected by this field\nand the ones listed in the namespaces field.\nnull selector and null or empty namespaces list means "this pod\'s namespace".\nAn empty selector ({}) matches all namespaces.', + type: 'object', + properties: { + matchExpressions: { + description: + 'matchExpressions is a list of label selector requirements. The requirements are ANDed.', + type: 'array', + items: { + description: + 'A label selector requirement is a selector that contains values, a key, and an operator that\nrelates the key and values.', + type: 'object', + required: ['key', 'operator'], + properties: { + key: { + description: 'key is the label key that the selector applies to.', + type: 'string' + }, + operator: { + description: + "operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist.", + type: 'string' + }, + values: { + description: + 'values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch.', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + matchLabels: { + description: + 'matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is "key", the\noperator is "In", and the values array contains only "value". The requirements are ANDed.', + type: 'object', + additionalProperties: { + type: 'string' + } + } + }, + 'x-kubernetes-map-type': 'atomic' + }, + namespaces: { + description: + 'namespaces specifies a static list of namespace names that the term applies to.\nThe term is applied to the union of the namespaces listed in this field\nand the ones selected by namespaceSelector.\nnull or empty namespaces list and null namespaceSelector means "this pod\'s namespace".', + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + }, + topologyKey: { + description: + 'This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching\nthe labelSelector in the specified namespaces, where co-located is defined as running on a node\nwhose value of the label with key topologyKey matches that of any node on which any of the\nselected pods is running.\nEmpty topologyKey is not allowed.', + type: 'string' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + } + } + } + } + }, + nodeSelector: { + description: + '`nodeSelector` allows scheduling of pods only onto nodes that have each of the specified labels.\nFor documentation, refer to https://kubernetes.io/docs/concepts/configuration/assign-pod-node/.', + type: 'object', + additionalProperties: { + type: 'string' + }, + 'x-kubernetes-map-type': 'atomic' + }, + priorityClassName: { + description: + "If specified, indicates the pod's priority. For documentation, refer to https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/#how-to-use-priority-and-preemption.\nIf not specified, default priority is used, or zero if there is no default.", + type: 'string' + }, + tolerations: { + description: + '`tolerations` is a list of tolerations that allow the pod to schedule onto nodes with matching taints.\nFor documentation, refer to https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#scheduling.', + type: 'array', + items: { + description: + 'The pod this Toleration is attached to tolerates any taint that matches\nthe triple using the matching operator .', + type: 'object', + properties: { + effect: { + description: + 'Effect indicates the taint effect to match. Empty means match all taint effects.\nWhen specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute.', + type: 'string' + }, + key: { + description: + 'Key is the taint key that the toleration applies to. Empty means match all taint keys.\nIf the key is empty, operator must be Exists; this combination means to match all values and all keys.', + type: 'string' + }, + operator: { + description: + "Operator represents a key's relationship to the value.\nValid operators are Exists and Equal. Defaults to Equal.\nExists is equivalent to wildcard for value, so that a pod can\ntolerate all taints of a particular category.", + type: 'string' + }, + tolerationSeconds: { + description: + 'TolerationSeconds represents the period of time the toleration (which must be\nof effect NoExecute, otherwise this field is ignored) tolerates the taint. By default,\nit is not set, which means tolerate the taint forever (do not evict). Zero and\nnegative values will be treated as 0 (evict immediately) by the system.', + type: 'integer', + format: 'int64' + }, + value: { + description: + 'Value is the taint value the toleration matches to.\nIf the operator is Exists, the value should be empty, otherwise just a regular string.', + type: 'string' + } + } + } + } + } + } + } + }, + enable: { + description: 'Enables the console plugin deployment.', + type: 'boolean', + default: true + }, + resources: { + description: + '`resources`, in terms of compute resources, required by this container.\nFor more information, see https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/', + type: 'object', + default: { + limits: { + memory: '100Mi' + }, + requests: { + cpu: '100m', + memory: '50Mi' + } + }, + properties: { + claims: { + description: + 'Claims lists the names of resources, defined in spec.resourceClaims,\nthat are used by this container.\n\nThis is an alpha field and requires enabling the\nDynamicResourceAllocation feature gate.\n\nThis field is immutable. It can only be set for containers.', + type: 'array', + items: { + description: 'ResourceClaim references one entry in PodSpec.ResourceClaims.', + type: 'object', + required: ['name'], + properties: { + name: { + description: + 'Name must match the name of one entry in pod.spec.resourceClaims of\nthe Pod where this field is used. It makes that resource available\ninside a container.', + type: 'string' + }, + request: { + description: + 'Request is the name chosen for a request in the referenced claim.\nIf empty, everything from the claim is made available, otherwise\nonly the result of this request.', + type: 'string' + } + } + }, + 'x-kubernetes-list-map-keys': ['name'], + 'x-kubernetes-list-type': 'map' + }, + limits: { + description: + 'Limits describes the maximum amount of compute resources allowed.\nMore info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/', + type: 'object', + additionalProperties: { + pattern: + '^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$', + anyOf: [ + { + type: 'integer' + }, + { + type: 'string' + } + ], + 'x-kubernetes-int-or-string': true + } + }, + requests: { + description: + 'Requests describes the minimum amount of compute resources required.\nIf Requests is omitted for a container, it defaults to Limits if that is explicitly specified,\notherwise to an implementation-defined value. Requests cannot exceed Limits.\nMore info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/', + type: 'object', + additionalProperties: { + pattern: + '^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$', + anyOf: [ + { + type: 'integer' + }, + { + type: 'string' + } + ], + 'x-kubernetes-int-or-string': true + } + } + } + }, + portNaming: { + description: '`portNaming` defines the configuration of the port-to-service name translation', + type: 'object', + default: { + enable: true + }, + properties: { + enable: { + description: 'Enable the console plugin port-to-service name translation', + type: 'boolean', + default: true + }, + portNames: { + description: + '`portNames` defines additional port names to use in the console,\nfor example, `portNames: {"3100": "loki"}`.', + type: 'object', + additionalProperties: { + type: 'string' + } + } + } + }, + quickFilters: { + description: '`quickFilters` configures quick filter presets for the Console plugin', + type: 'array', + default: [ + { + default: true, + filter: { + flow_layer: '"app"' + }, + name: 'Applications' + }, + { + filter: { + flow_layer: '"infra"' + }, + name: 'Infrastructure' + }, + { + default: true, + filter: { + dst_kind: '"Pod"', + src_kind: '"Pod"' + }, + name: 'Pods network' + }, + { + filter: { + dst_kind: '"Service"' + }, + name: 'Services network' + } + ], + items: { + description: "`QuickFilter` defines preset configuration for Console's quick filters", + type: 'object', + required: ['filter', 'name'], + properties: { + default: { + description: '`default` defines whether this filter should be active by default or not', + type: 'boolean' + }, + filter: { + description: + '`filter` is a set of keys and values to be set when this filter is selected. Each key can relate to a list of values using a coma-separated string,\nfor example, `filter: {"src_namespace": "namespace1,namespace2"}`.', + type: 'object', + additionalProperties: { + type: 'string' + } + }, + name: { + description: 'Name of the filter, that is displayed in the Console', + type: 'string' + } + } + } + }, + imagePullPolicy: { + description: '`imagePullPolicy` is the Kubernetes pull policy for the image defined above', + type: 'string', + default: 'IfNotPresent', + enum: ['IfNotPresent', 'Always', 'Never'] + }, + autoscaler: { + description: '`autoscaler` spec of a horizontal pod autoscaler to set up for the plugin Deployment.', + type: 'object', + properties: { + maxReplicas: { + description: + '`maxReplicas` is the upper limit for the number of pods that can be set by the autoscaler; cannot be smaller than MinReplicas.', + type: 'integer', + format: 'int32', + default: 3 + }, + metrics: { + description: + 'Metrics used by the pod autoscaler. For documentation, refer to https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/horizontal-pod-autoscaler-v2/', + type: 'array', + items: { + type: 'object', + required: ['type'], + properties: { + containerResource: { + type: 'object', + required: ['container', 'name', 'target'], + properties: { + container: { + type: 'string' + }, + name: { + type: 'string' + }, + target: { + type: 'object', + required: ['type'], + properties: { + averageUtilization: { + type: 'integer', + format: 'int32' + }, + averageValue: { + pattern: + '^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$', + anyOf: [ + { + type: 'integer' + }, + { + type: 'string' + } + ], + 'x-kubernetes-int-or-string': true + }, + type: { + type: 'string' + }, + value: { + pattern: + '^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$', + anyOf: [ + { + type: 'integer' + }, + { + type: 'string' + } + ], + 'x-kubernetes-int-or-string': true + } + } + } + } + }, + external: { + type: 'object', + required: ['metric', 'target'], + properties: { + metric: { + type: 'object', + required: ['name'], + properties: { + name: { + type: 'string' + }, + selector: { + type: 'object', + properties: { + matchExpressions: { + type: 'array', + items: { + type: 'object', + required: ['key', 'operator'], + properties: { + key: { + type: 'string' + }, + operator: { + type: 'string' + }, + values: { + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + matchLabels: { + type: 'object', + additionalProperties: { + type: 'string' + } + } + }, + 'x-kubernetes-map-type': 'atomic' + } + } + }, + target: { + type: 'object', + required: ['type'], + properties: { + averageUtilization: { + type: 'integer', + format: 'int32' + }, + averageValue: { + pattern: + '^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$', + anyOf: [ + { + type: 'integer' + }, + { + type: 'string' + } + ], + 'x-kubernetes-int-or-string': true + }, + type: { + type: 'string' + }, + value: { + pattern: + '^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$', + anyOf: [ + { + type: 'integer' + }, + { + type: 'string' + } + ], + 'x-kubernetes-int-or-string': true + } + } + } + } + }, + object: { + type: 'object', + required: ['describedObject', 'metric', 'target'], + properties: { + describedObject: { + type: 'object', + required: ['kind', 'name'], + properties: { + apiVersion: { + type: 'string' + }, + kind: { + type: 'string' + }, + name: { + type: 'string' + } + } + }, + metric: { + type: 'object', + required: ['name'], + properties: { + name: { + type: 'string' + }, + selector: { + type: 'object', + properties: { + matchExpressions: { + type: 'array', + items: { + type: 'object', + required: ['key', 'operator'], + properties: { + key: { + type: 'string' + }, + operator: { + type: 'string' + }, + values: { + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + matchLabels: { + type: 'object', + additionalProperties: { + type: 'string' + } + } + }, + 'x-kubernetes-map-type': 'atomic' + } + } + }, + target: { + type: 'object', + required: ['type'], + properties: { + averageUtilization: { + type: 'integer', + format: 'int32' + }, + averageValue: { + pattern: + '^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$', + anyOf: [ + { + type: 'integer' + }, + { + type: 'string' + } + ], + 'x-kubernetes-int-or-string': true + }, + type: { + type: 'string' + }, + value: { + pattern: + '^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$', + anyOf: [ + { + type: 'integer' + }, + { + type: 'string' + } + ], + 'x-kubernetes-int-or-string': true + } + } + } + } + }, + pods: { + type: 'object', + required: ['metric', 'target'], + properties: { + metric: { + type: 'object', + required: ['name'], + properties: { + name: { + type: 'string' + }, + selector: { + type: 'object', + properties: { + matchExpressions: { + type: 'array', + items: { + type: 'object', + required: ['key', 'operator'], + properties: { + key: { + type: 'string' + }, + operator: { + type: 'string' + }, + values: { + type: 'array', + items: { + type: 'string' + }, + 'x-kubernetes-list-type': 'atomic' + } + } + }, + 'x-kubernetes-list-type': 'atomic' + }, + matchLabels: { + type: 'object', + additionalProperties: { + type: 'string' + } + } + }, + 'x-kubernetes-map-type': 'atomic' + } + } + }, + target: { + type: 'object', + required: ['type'], + properties: { + averageUtilization: { + type: 'integer', + format: 'int32' + }, + averageValue: { + pattern: + '^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$', + anyOf: [ + { + type: 'integer' + }, + { + type: 'string' + } + ], + 'x-kubernetes-int-or-string': true + }, + type: { + type: 'string' + }, + value: { + pattern: + '^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$', + anyOf: [ + { + type: 'integer' + }, + { + type: 'string' + } + ], + 'x-kubernetes-int-or-string': true + } + } + } + } + }, + resource: { + type: 'object', + required: ['name', 'target'], + properties: { + name: { + type: 'string' + }, + target: { + type: 'object', + required: ['type'], + properties: { + averageUtilization: { + type: 'integer', + format: 'int32' + }, + averageValue: { + pattern: + '^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$', + anyOf: [ + { + type: 'integer' + }, + { + type: 'string' + } + ], + 'x-kubernetes-int-or-string': true + }, + type: { + type: 'string' + }, + value: { + pattern: + '^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$', + anyOf: [ + { + type: 'integer' + }, + { + type: 'string' + } + ], + 'x-kubernetes-int-or-string': true + } + } + } + } + }, + type: { + type: 'string' + } + } + } + }, + minReplicas: { + description: + '`minReplicas` is the lower limit for the number of replicas to which the autoscaler\ncan scale down. It defaults to 1 pod. minReplicas is allowed to be 0 if the\nalpha feature gate HPAScaleToZero is enabled and at least one Object or External\nmetric is configured. Scaling is active as long as at least one metric value is\navailable.', + type: 'integer', + format: 'int32' + }, + status: { + description: + '`status` describes the desired status regarding deploying an horizontal pod autoscaler.\n- `Disabled` does not deploy an horizontal pod autoscaler.\n- `Enabled` deploys an horizontal pod autoscaler.', + type: 'string', + default: 'Disabled', + enum: ['Disabled', 'Enabled'] + } + } + }, + replicas: { + description: '`replicas` defines the number of replicas (pods) to start.', + type: 'integer', + format: 'int32', + default: 1, + minimum: 0 + } + } + }, + networkPolicy: { + description: + '`networkPolicy` defines ingress network policy settings for network observability components isolation.', + type: 'object', + properties: { + enable: { + description: + 'Deploy network policies on the namespaces used by network observability (main and privileged). It is disabled by default.\nThese network policies better isolate the network observability components to prevent undesired connections to them.\nTo increase the security of connections, enable this option or create your own network policy.', + type: 'boolean' + }, + additionalNamespaces: { + description: + '`additionalNamespaces` contains additional namespaces allowed to connect to the network observability namespace.\nIt provides flexibility in the network policy configuration, but if you need a more specific\nconfiguration, you can disable it and install your own instead.', + type: 'array', + items: { + type: 'string' + } + } + } + }, + exporters: { + description: '`exporters` defines additional optional exporters for custom consumption or storage.', + type: 'array', + items: { + description: '`FlowCollectorExporter` defines an additional exporter to send enriched flows to.', + type: 'object', + required: ['type'], + properties: { + ipfix: { + description: 'IPFIX configuration, such as the IP address and port to send enriched IPFIX flows to.', + type: 'object', + required: ['targetHost', 'targetPort'], + properties: { + targetHost: { + description: 'Address of the IPFIX external receiver.', + type: 'string', + default: '' + }, + targetPort: { + description: 'Port for the IPFIX external receiver.', + type: 'integer' + }, + transport: { + description: + 'Transport protocol (`TCP` or `UDP`) to be used for the IPFIX connection, defaults to `TCP`.', + type: 'string', + enum: ['TCP', 'UDP'] + } + } + }, + kafka: { + description: 'Kafka configuration, such as the address and topic, to send enriched flows to.', + type: 'object', + required: ['address', 'topic'], + properties: { + address: { + description: 'Address of the Kafka server', + type: 'string', + default: '' + }, + sasl: { + description: 'SASL authentication configuration. [Unsupported (*)].', + type: 'object', + properties: { + clientIDReference: { + description: 'Reference to the secret or config map containing the client ID', + type: 'object', + properties: { + file: { + description: 'File name within the config map or secret.', + type: 'string' + }, + name: { + description: 'Name of the config map or secret containing the file.', + type: 'string' + }, + namespace: { + description: + 'Namespace of the config map or secret containing the file. If omitted, the default is to use the same namespace as where network observability is deployed.\nIf the namespace is different, the config map or the secret is copied so that it can be mounted as required.', + type: 'string', + default: '' + }, + type: { + description: 'Type for the file reference: `configmap` or `secret`.', + type: 'string', + enum: ['configmap', 'secret'] + } + } + }, + clientSecretReference: { + description: 'Reference to the secret or config map containing the client secret', + type: 'object', + properties: { + file: { + description: 'File name within the config map or secret.', + type: 'string' + }, + name: { + description: 'Name of the config map or secret containing the file.', + type: 'string' + }, + namespace: { + description: + 'Namespace of the config map or secret containing the file. If omitted, the default is to use the same namespace as where network observability is deployed.\nIf the namespace is different, the config map or the secret is copied so that it can be mounted as required.', + type: 'string', + default: '' + }, + type: { + description: 'Type for the file reference: `configmap` or `secret`.', + type: 'string', + enum: ['configmap', 'secret'] + } + } + }, + type: { + description: 'Type of SASL authentication to use, or `Disabled` if SASL is not used', + type: 'string', + default: 'Disabled', + enum: ['Disabled', 'Plain', 'ScramSHA512'] + } + } + }, + tls: { + description: + 'TLS client configuration. When using TLS, verify that the address matches the Kafka port used for TLS, generally 9093.', + type: 'object', + properties: { + caCert: { + description: '`caCert` defines the reference of the certificate for the Certificate Authority.', + type: 'object', + properties: { + certFile: { + description: + '`certFile` defines the path to the certificate file name within the config map or secret.', + type: 'string' + }, + certKey: { + description: + '`certKey` defines the path to the certificate private key file name within the config map or secret. Omit when the key is not necessary.', + type: 'string' + }, + name: { + description: 'Name of the config map or secret containing certificates.', + type: 'string' + }, + namespace: { + description: + 'Namespace of the config map or secret containing certificates. If omitted, the default is to use the same namespace as where network observability is deployed.\nIf the namespace is different, the config map or the secret is copied so that it can be mounted as required.', + type: 'string', + default: '' + }, + type: { + description: 'Type for the certificate reference: `configmap` or `secret`.', + type: 'string', + enum: ['configmap', 'secret'] + } + } + }, + enable: { + description: 'Enable TLS', + type: 'boolean', + default: false + }, + insecureSkipVerify: { + description: + '`insecureSkipVerify` allows skipping client-side verification of the server certificate.\nIf set to `true`, the `caCert` field is ignored.', + type: 'boolean', + default: false + }, + userCert: { + description: + '`userCert` defines the user certificate reference and is used for mTLS. When you use one-way TLS, you can ignore this property.', + type: 'object', + properties: { + certFile: { + description: + '`certFile` defines the path to the certificate file name within the config map or secret.', + type: 'string' + }, + certKey: { + description: + '`certKey` defines the path to the certificate private key file name within the config map or secret. Omit when the key is not necessary.', + type: 'string' + }, + name: { + description: 'Name of the config map or secret containing certificates.', + type: 'string' + }, + namespace: { + description: + 'Namespace of the config map or secret containing certificates. If omitted, the default is to use the same namespace as where network observability is deployed.\nIf the namespace is different, the config map or the secret is copied so that it can be mounted as required.', + type: 'string', + default: '' + }, + type: { + description: 'Type for the certificate reference: `configmap` or `secret`.', + type: 'string', + enum: ['configmap', 'secret'] + } + } + } + } + }, + topic: { + description: 'Kafka topic to use. It must exist. network observability does not create it.', + type: 'string', + default: '' + } + } + }, + openTelemetry: { + description: + 'OpenTelemetry configuration, such as the IP address and port to send enriched logs or metrics to.', + type: 'object', + required: ['targetHost', 'targetPort'], + properties: { + fieldsMapping: { + description: + 'Custom fields mapping to an OpenTelemetry conformant format.\nBy default, network observability format proposal is used: https://github.com/rhobs/observability-data-model/blob/main/network-observability.md#format-proposal .\nAs there is currently no accepted standard for L3 or L4 enriched network logs, you can freely override it with your own.', + type: 'array', + items: { + type: 'object', + properties: { + input: { + type: 'string' + }, + multiplier: { + type: 'integer' + }, + output: { + type: 'string' + } + } + } + }, + headers: { + description: 'Headers to add to messages (optional)', + type: 'object', + additionalProperties: { + type: 'string' + } + }, + logs: { + description: 'OpenTelemetry configuration for logs.', + type: 'object', + properties: { + enable: { + description: 'Set `enable` to `true` to send logs to an OpenTelemetry receiver.', + type: 'boolean', + default: true + } + } + }, + metrics: { + description: 'OpenTelemetry configuration for metrics.', + type: 'object', + properties: { + enable: { + description: 'Set `enable` to `true` to send metrics to an OpenTelemetry receiver.', + type: 'boolean', + default: true + }, + pushTimeInterval: { + description: 'Specify how often metrics are sent to a collector.', + type: 'string', + default: '20s' + } + } + }, + protocol: { + description: + 'Protocol of the OpenTelemetry connection. The available options are `http` and `grpc`.', + type: 'string', + enum: ['http', 'grpc'] + }, + targetHost: { + description: 'Address of the OpenTelemetry receiver.', + type: 'string', + default: '' + }, + targetPort: { + description: 'Port for the OpenTelemetry receiver.', + type: 'integer' + }, + tls: { + description: 'TLS client configuration.', + type: 'object', + properties: { + caCert: { + description: '`caCert` defines the reference of the certificate for the Certificate Authority.', + type: 'object', + properties: { + certFile: { + description: + '`certFile` defines the path to the certificate file name within the config map or secret.', + type: 'string' + }, + certKey: { + description: + '`certKey` defines the path to the certificate private key file name within the config map or secret. Omit when the key is not necessary.', + type: 'string' + }, + name: { + description: 'Name of the config map or secret containing certificates.', + type: 'string' + }, + namespace: { + description: + 'Namespace of the config map or secret containing certificates. If omitted, the default is to use the same namespace as where network observability is deployed.\nIf the namespace is different, the config map or the secret is copied so that it can be mounted as required.', + type: 'string', + default: '' + }, + type: { + description: 'Type for the certificate reference: `configmap` or `secret`.', + type: 'string', + enum: ['configmap', 'secret'] + } + } + }, + enable: { + description: 'Enable TLS', + type: 'boolean', + default: false + }, + insecureSkipVerify: { + description: + '`insecureSkipVerify` allows skipping client-side verification of the server certificate.\nIf set to `true`, the `caCert` field is ignored.', + type: 'boolean', + default: false + }, + userCert: { + description: + '`userCert` defines the user certificate reference and is used for mTLS. When you use one-way TLS, you can ignore this property.', + type: 'object', + properties: { + certFile: { + description: + '`certFile` defines the path to the certificate file name within the config map or secret.', + type: 'string' + }, + certKey: { + description: + '`certKey` defines the path to the certificate private key file name within the config map or secret. Omit when the key is not necessary.', + type: 'string' + }, + name: { + description: 'Name of the config map or secret containing certificates.', + type: 'string' + }, + namespace: { + description: + 'Namespace of the config map or secret containing certificates. If omitted, the default is to use the same namespace as where network observability is deployed.\nIf the namespace is different, the config map or the secret is copied so that it can be mounted as required.', + type: 'string', + default: '' + }, + type: { + description: 'Type for the certificate reference: `configmap` or `secret`.', + type: 'string', + enum: ['configmap', 'secret'] + } + } + } + } + } + } + }, + type: { + description: + '`type` selects the type of exporters. The available options are `Kafka`, `IPFIX`, and `OpenTelemetry`.', + type: 'string', + enum: ['Kafka', 'IPFIX', 'OpenTelemetry'] + } + } + } + } + } + } + } +}; + +export const FlowMetricSchema: RJSFSchema | any = { + title: 'FlowMetric', + description: 'The API allowing to create custom metrics from the collected flow logs.', + type: 'object', + properties: { + apiVersion: { + type: 'string', + description: + 'APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + }, + kind: { + type: 'string', + description: + 'Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + }, + metadata: { + type: 'object', + properties: { + namespace: { + type: 'string', + default: 'netobserv' + }, + name: { + type: 'string', + default: 'example' + }, + labels: { + type: 'object', + properties: {}, + additionalProperties: { + type: 'string' + } + } + }, + required: ['name'] + }, + spec: { + type: 'object', + description: + 'FlowMetricSpec defines the desired state of FlowMetric\nThe provided API allows you to customize these metrics according to your needs.
\nWhen adding new metrics or modifying existing labels, you must carefully monitor the memory\nusage of Prometheus workloads as this could potentially have a high impact. Cf https://rhobs-handbook.netlify.app/products/openshiftmonitoring/telemetry.md/#what-is-the-cardinality-of-a-metric
\nTo check the cardinality of all NetObserv metrics, run as `promql`: `count({__name__=~"netobserv.*"}) by (__name__)`.', + required: ['metricName', 'type'], + properties: { + metricName: { + description: 'Name of the metric. In Prometheus, it is automatically prefixed with "netobserv_".', + type: 'string' + }, + type: { + description: + 'Metric type: "Counter" or "Histogram".\nUse "Counter" for any value that increases over time and on which you can compute a rate, such as Bytes or Packets.\nUse "Histogram" for any value that must be sampled independently, such as latencies.', + type: 'string', + enum: ['Counter', 'Histogram'] + }, + buckets: { + description: + 'A list of buckets to use when `type` is "Histogram". The list must be parsable as floats. When not set, Prometheus default buckets are used.', + type: 'array', + items: { + type: 'string' + } + }, + valueField: { + description: + '`valueField` is the flow field that must be used as a value for this metric. This field must hold numeric values.\nLeave empty to count flows rather than a specific value per flow.\nRefer to the documentation for the list of available fields: https://docs.openshift.com/container-platform/latest/observability/network_observability/json-flows-format-reference.html.', + type: 'string' + }, + divider: { + description: 'When nonzero, scale factor (divider) of the value. Metric value = Flow value / Divider.', + type: 'string' + }, + labels: { + description: + '`labels` is a list of fields that should be used as Prometheus labels, also known as dimensions.\nFrom choosing labels results the level of granularity of this metric, and the available aggregations at query time.\nIt must be done carefully as it impacts the metric cardinality (cf https://rhobs-handbook.netlify.app/products/openshiftmonitoring/telemetry.md/#what-is-the-cardinality-of-a-metric).\nIn general, avoid setting very high cardinality labels such as IP or MAC addresses.\n"SrcK8S_OwnerName" or "DstK8S_OwnerName" should be preferred over "SrcK8S_Name" or "DstK8S_Name" as much as possible.\nRefer to the documentation for the list of available fields: https://docs.openshift.com/container-platform/latest/observability/network_observability/json-flows-format-reference.html.', + type: 'array', + items: { + type: 'string' + } + }, + flatten: { + description: + '`flatten` is a list of array-type fields that must be flattened, such as Interfaces or NetworkEvents. Flattened fields generate one metric per item in that field.\nFor instance, when flattening `Interfaces` on a bytes counter, a flow having Interfaces [br-ex, ens5] increases one counter for `br-ex` and another for `ens5`.', + type: 'array', + items: { + type: 'string' + } + }, + remap: { + description: + 'Set the `remap` property to use different names for the generated metric labels than the flow fields. Use the origin flow fields as keys, and the desired label names as values.', + type: 'string' + }, + direction: { + description: + 'Filter for ingress, egress or any direction flows.\nWhen set to `Ingress`, it is equivalent to adding the regular expression filter on `FlowDirection`: `0|2`.\nWhen set to `Egress`, it is equivalent to adding the regular expression filter on `FlowDirection`: `1|2`.', + type: 'string', + default: 'Any', + enum: ['Any', 'Egress', 'Ingress'] + }, + filters: { + description: + '`filters` is a list of fields and values used to restrict which flows are taken into account. Oftentimes, these filters must\nbe used to eliminate duplicates: `Duplicate != "true"` and `FlowDirection = "0"`.\nRefer to the documentation for the list of available fields: https://docs.openshift.com/container-platform/latest/observability/network_observability/json-flows-format-reference.html.', + type: 'array', + items: { + type: 'object', + required: ['field', 'matchType'], + properties: { + field: { + description: 'Name of the field to filter on', + type: 'string' + }, + matchType: { + description: 'Type of matching to apply', + type: 'string', + default: 'Equal', + enum: ['Equal', 'NotEqual', 'Presence', 'Absence', 'MatchRegex', 'NotMatchRegex'] + }, + value: { + description: + 'Value to filter on. When `matchType` is `Equal` or `NotEqual`, you can use field injection with `$(SomeField)` to refer to any other field of the flow.', + type: 'string' + } + } + } + }, + charts: { + description: 'Charts configuration, for the OpenShift Console in the administrator view, Dashboards menu.', + type: 'array', + items: { + description: 'Configures charts / dashboard generation associated to a metric', + type: 'object', + required: ['dashboardName', 'queries', 'title', 'type'], + properties: { + dashboardName: { + description: + 'Name of the containing dashboard. If this name does not refer to an existing dashboard, a new dashboard is created.', + type: 'string', + default: 'Main' + }, + sectionName: { + description: + 'Name of the containing dashboard section. If this name does not refer to an existing section, a new section is created.\nIf `sectionName` is omitted or empty, the chart is placed in the global top section.', + type: 'string' + }, + title: { + description: 'Title of the chart.', + type: 'string' + }, + unit: { + description: + 'Unit of this chart. Only a few units are currently supported. Leave empty to use generic number.', + type: 'string', + enum: ['bytes', 'seconds', 'Bps', 'pps', 'percent', ''] + }, + type: { + description: 'Type of the chart.', + type: 'string', + enum: ['SingleStat', 'Line', 'StackArea'] + }, + queries: { + description: + 'List of queries to be displayed on this chart. If `type` is `SingleStat` and multiple queries are provided,\nthis chart is automatically expanded in several panels (one per query).', + type: 'array', + items: { + description: 'Configures PromQL queries', + type: 'object', + required: ['legend', 'promQL', 'top'], + properties: { + promQL: { + description: + 'The `promQL` query to be run against Prometheus. If the chart `type` is `SingleStat`, this query should only return\na single timeseries. For other types, a top 7 is displayed.\nYou can use `$METRIC` to refer to the metric defined in this resource. For example: `sum(rate($METRIC[2m]))`.\nTo learn more about `promQL`, refer to the Prometheus documentation: https://prometheus.io/docs/prometheus/latest/querying/basics/', + type: 'string' + }, + legend: { + description: + 'The query legend that applies to each timeseries represented in this chart. When multiple timeseries are displayed, you should set a legend\nthat distinguishes each of them. It can be done with the following format: `{{ Label }}`. For example, if the `promQL` groups timeseries per\nlabel such as: `sum(rate($METRIC[2m])) by (Label1, Label2)`, you may write as the legend: `Label1={{ Label1 }}, Label2={{ Label2 }}`.', + type: 'string' + }, + top: { + description: 'Top N series to display per timestamp. Does not apply to `SingleStat` chart type.', + type: 'integer', + default: 7, + minimum: 1 + } + } + } + } + } + } + } + } + } + } +}; diff --git a/web/src/components/forms/config/templates.ts b/web/src/components/forms/config/templates.ts new file mode 100644 index 000000000..4ad0a84b8 --- /dev/null +++ b/web/src/components/forms/config/templates.ts @@ -0,0 +1,197 @@ +import { K8sResourceKind } from '@openshift-console/dynamic-plugin-sdk'; +import { safeYAMLToJS } from '../../../utils/yaml'; + +export const FlowCollector = ` +apiVersion: flows.netobserv.io/v1beta2 +kind: FlowCollector +metadata: + name: cluster +spec: + namespace: netobserv + deploymentModel: Direct + networkPolicy: + enable: false + additionalNamespaces: [] + agent: + type: eBPF + ebpf: + imagePullPolicy: IfNotPresent + logLevel: info + sampling: 50 + cacheActiveTimeout: 5s + cacheMaxFlows: 100000 + privileged: false + interfaces: [] + excludeInterfaces: + - lo + kafkaBatchSize: 1048576 + metrics: + server: + port: 9400 + resources: + requests: + memory: 50Mi + cpu: 100m + limits: + memory: 800Mi + kafka: + address: kafka-cluster-kafka-bootstrap.netobserv + topic: network-flows + tls: + enable: false + caCert: + type: secret + name: kafka-cluster-cluster-ca-cert + certFile: ca.crt + userCert: + type: secret + name: flp-kafka + certFile: user.crt + certKey: user.key + processor: + imagePullPolicy: IfNotPresent + logLevel: info + logTypes: Flows + metrics: + server: + port: 9401 + disableAlerts: [] + kafkaConsumerReplicas: 3 + kafkaConsumerAutoscaler: null + kafkaConsumerQueueCapacity: 1000 + kafkaConsumerBatchSize: 10485760 + resources: + requests: + memory: 100Mi + cpu: 100m + limits: + memory: 800Mi + loki: + enable: true + mode: Monolithic + monolithic: + url: 'http://loki.netobserv.svc:3100/' + tenantID: netobserv + tls: + enable: false + caCert: + type: configmap + name: loki-gateway-ca-bundle + certFile: service-ca.crt + lokiStack: + name: loki + readTimeout: 30s + writeTimeout: 10s + writeBatchWait: 1s + writeBatchSize: 10485760 + prometheus: + querier: + enable: true + mode: Auto + timeout: 30s + consolePlugin: + enable: true + imagePullPolicy: IfNotPresent + logLevel: info + portNaming: + enable: true + portNames: + '3100': loki + quickFilters: + - name: Applications + filter: + flow_layer: '"app"' + default: true + - name: Infrastructure + filter: + flow_layer: '"infra"' + - name: Pods network + filter: + src_kind: '"Pod"' + dst_kind: '"Pod"' + default: true + - name: Services network + filter: + dst_kind: '"Service"' + resources: + requests: + memory: 50Mi + cpu: 100m + limits: + memory: 100Mi + exporters: [] +`; + +let flowCollectorJS: K8sResourceKind | null = null; +export const GetFlowCollectorJS = (): K8sResourceKind => { + if (flowCollectorJS === null) { + flowCollectorJS = safeYAMLToJS(FlowCollector); + } + return flowCollectorJS!; +}; + +export const FlowMetric = ` +apiVersion: flows.netobserv.io/v1alpha1 +kind: FlowMetric +metadata: + labels: + app.kubernetes.io/name: flowmetric + app.kubernetes.io/instance: flowmetric-sample + app.kubernetes.io/part-of: netobserv-operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: netobserv-operator + name: flowmetric-sample + namespace: netobserv +spec: + metricName: cluster_external_ingress_bytes_total + type: Counter + valueField: Bytes + direction: Ingress + labels: + - DstK8S_HostName + - DstK8S_Namespace + - DstK8S_OwnerName + - DstK8S_OwnerType + filters: + - field: SrcSubnetLabel + matchType: Absence + charts: + - dashboardName: Main + title: External ingress traffic + unit: Bps + type: SingleStat + queries: + - promQL: 'sum(rate($METRIC[2m]))' + legend: '' + - dashboardName: Main + sectionName: External + title: Top external ingress traffic per workload + unit: Bps + type: StackArea + queries: + - promQL: >- + sum(rate($METRIC{DstK8S_Namespace!=""}[2m])) by (DstK8S_Namespace, + DstK8S_OwnerName) + legend: '{{DstK8S_Namespace}} / {{DstK8S_OwnerName}}' +`; + +// use an alternative sample for forms to avoid forcing the user to remove the filters / queries +export const FlowMetricDefaultForm = ` +apiVersion: flows.netobserv.io/v1alpha1 +kind: FlowMetric +metadata: + name: flowmetric-sample + namespace: netobserv +spec: + type: Counter + valueField: Bytes + direction: Ingress +`; + +let flowMetricJS: K8sResourceKind | null = null; +export const GetFlowMetricJS = (): K8sResourceKind => { + if (flowMetricJS === null) { + flowMetricJS = safeYAMLToJS(FlowMetricDefaultForm); + } + return flowMetricJS!; +}; diff --git a/web/src/components/forms/config/uiSchema.ts b/web/src/components/forms/config/uiSchema.ts new file mode 100644 index 000000000..86198e084 --- /dev/null +++ b/web/src/components/forms/config/uiSchema.ts @@ -0,0 +1,1915 @@ +/* eslint-disable max-len */ +import { UiSchema } from '@rjsf/utils'; + +// Keep the UISchemas ordered for form display + +export const FlowCollectorUISchema: UiSchema = { + 'ui:title': 'FlowCollector', + 'ui:description': 'The API for the network flows collection, which pilots and configures the underlying deployments.', + 'ui:flat': 'true', + metadata: { + 'ui:title': 'Metadata', + 'ui:widget': 'hidden', + name: { + 'ui:title': 'Name', + 'ui:widget': 'hidden' + }, + labels: { + 'ui:widget': 'hidden' + }, + 'ui:order': ['name', 'labels', '*'] + }, + spec: { + namespace: { + 'ui:title': 'Namespace' + }, + deploymentModel: { + 'ui:title': 'Deployment model', + 'ui:description': 'The desired type of deployment for flow processing.' + }, + kafka: { + 'ui:title': 'Kafka configuration', + 'ui:description': 'Kafka as a broker as part of the flow collection pipeline.', + 'ui:dependency': { + controlFieldPath: ['deploymentModel'], + controlFieldValue: 'Kafka', + controlFieldName: 'deploymentModel' + }, + address: { + 'ui:title': 'Address' + }, + topic: { + 'ui:title': 'Topic' + }, + tls: { + 'ui:title': 'TLS configuration', + enable: { + 'ui:title': 'Use TLS' + }, + caCert: { + 'ui:title': 'CA certificate', + 'ui:order': ['certFile', 'certKey', 'name', 'namespace', 'type'] + }, + userCert: { + 'ui:title': 'User certificate when using mTLS', + 'ui:order': ['certFile', 'certKey', 'name', 'namespace', 'type'] + }, + insecureSkipVerify: { + 'ui:title': 'Insecure skip verify' + }, + 'ui:order': ['enable', 'caCert', 'userCert', 'insecureSkipVerify'] + }, + sasl: { + 'ui:title': 'SASL', + 'ui:description': 'SASL authentication configuration. Unsupported.', + type: { + 'ui:title': 'Type' + }, + clientIDReference: { + 'ui:title': 'Client ID reference', + 'ui:order': ['file', 'name', 'namespace', 'type'] + }, + clientSecretReference: { + 'ui:title': 'Client secret reference', + 'ui:order': ['file', 'name', 'namespace', 'type'] + }, + 'ui:order': ['type', 'clientIDReference', 'clientSecretReference'] + }, + 'ui:order': ['address', 'topic', 'tls', 'sasl'] + }, + agent: { + 'ui:title': 'Agent configuration', + 'ui:description': 'Flows extraction.', + type: { + 'ui:widget': 'hidden' + }, + ipfix: { + 'ui:widget': 'hidden' + }, + ebpf: { + 'ui:title': 'eBPF Agent configuration', + 'ui:description': 'Settings related to the eBPF-based flow reporter.', + 'ui:dependency': { + controlFieldPath: ['agent', 'type'], + controlFieldValue: 'eBPF', + controlFieldName: 'type' + }, + 'ui:flat': 'true', + sampling: { + 'ui:title': 'Sampling' + }, + privileged: { + 'ui:title': 'Privileged mode' + }, + features: { + 'ui:title': 'Features', + 'ui:widget': 'arrayCheckboxes', + 'ui:descriptionFirst': 'true' + }, + flowFilter: { + 'ui:title': 'Filters', + 'ui:description': 'The eBPF agent configuration regarding flow filtering.', + 'ui:widget': 'hidden', + enable: { + 'ui:title': 'Enable flow filtering' + }, + tcpFlags: { + 'ui:widget': 'hidden' + }, + sampling: { + 'ui:widget': 'hidden' + }, + peerIP: { + 'ui:widget': 'hidden' + }, + icmpCode: { + 'ui:widget': 'hidden' + }, + pktDrops: { + 'ui:widget': 'hidden' + }, + destPorts: { + 'ui:widget': 'hidden' + }, + ports: { + 'ui:widget': 'hidden' + }, + cidr: { + 'ui:widget': 'hidden' + }, + action: { + 'ui:widget': 'hidden' + }, + peerCIDR: { + 'ui:widget': 'hidden' + }, + sourcePorts: { + 'ui:widget': 'hidden' + }, + icmpType: { + 'ui:widget': 'hidden' + }, + protocol: { + 'ui:widget': 'hidden' + }, + direction: { + 'ui:widget': 'hidden' + }, + rules: { + 'ui:title': 'Rules', + 'ui:description': + 'A list of filtering rules on the eBPF Agents.\nWhen filtering is enabled, by default, flows that don\'t match any rule are rejected.\nTo change the default, you can define a rule that accepts everything: `{ action: "Accept", cidr: "0.0.0.0/0" }`, and then refine with rejecting rules. Unsupported.', + items: { + 'ui:order': [ + 'tcpFlags', + 'sampling', + 'peerIP', + 'icmpCode', + 'pktDrops', + 'destPorts', + 'ports', + 'cidr', + 'action', + 'peerCIDR', + 'sourcePorts', + 'icmpType', + 'protocol', + 'direction' + ] + } + }, + 'ui:order': [ + 'enable', + 'rules', + 'tcpFlags', + 'sampling', + 'peerIP', + 'icmpCode', + 'pktDrops', + 'destPorts', + 'ports', + 'cidr', + 'action', + 'peerCIDR', + 'sourcePorts', + 'icmpType', + 'protocol', + 'direction' + ] + }, + interfaces: { + 'ui:title': 'Interfaces', + 'ui:description': + 'The interface names from where flows are collected. If empty, the agent\nfetches all the interfaces in the system, excepting the ones listed in `excludeInterfaces`.\nAn entry enclosed by slashes, such as `/br-/`, is matched as a regular expression.\nOtherwise it is matched as a case-sensitive string.' + }, + excludeInterfaces: { + 'ui:title': 'Exclude interfaces', + 'ui:description': + 'The interface names that are excluded from flow tracing.\nAn entry enclosed by slashes, such as `/br-/`, is matched as a regular expression.\nOtherwise it is matched as a case-sensitive string.' + }, + logLevel: { + 'ui:title': 'Log level', + 'ui:description': 'The log level for the network observability eBPF Agent' + }, + imagePullPolicy: { + 'ui:title': 'Image pull policy', + 'ui:description': 'The Kubernetes pull policy for the image defined above' + }, + metrics: { + 'ui:title': 'Metrics', + 'ui:description': 'The eBPF agent configuration regarding metrics.', + enable: { + 'ui:widget': 'hidden' + }, + disableAlerts: { + 'ui:title': 'Disable alerts' + }, + server: { + 'ui:title': 'Server', + port: { + 'ui:title': 'Port' + }, + 'ui:order': ['port', 'tls'], + tls: { + 'ui:order': ['type', 'insecureSkipVerify', 'provided', 'providedCaFile'], + provided: { + 'ui:order': ['certFile', 'certKey', 'name', 'namespace', 'type'] + }, + providedCaFile: { + 'ui:order': ['file', 'name', 'namespace', 'type'] + } + } + }, + 'ui:order': ['enable', 'disableAlerts', 'server'] + }, + cacheMaxFlows: { + 'ui:title': 'Cache max flows', + 'ui:description': + 'The max number of flows in an aggregate; when reached, the reporter sends the flows.\nIncreasing `cacheMaxFlows` and `cacheActiveTimeout` can decrease the network traffic overhead and the CPU load,\nhowever you can expect higher memory consumption and an increased latency in the flow collection.' + }, + cacheActiveTimeout: { + 'ui:title': 'Cache active timeout', + 'ui:description': + 'The max period during which the reporter aggregates flows before sending.\nIncreasing `cacheMaxFlows` and `cacheActiveTimeout` can decrease the network traffic overhead and the CPU load,\nhowever you can expect higher memory consumption and an increased latency in the flow collection.' + }, + kafkaBatchSize: { + 'ui:title': 'Kafka batch size', + 'ui:description': + 'Limits the maximum size of a request in bytes before being sent to a partition. Ignored when not using Kafka. Default: 1MB.', + 'ui:dependency': { + controlFieldPath: ['deploymentModel'], + controlFieldValue: 'Kafka', + controlFieldName: 'deploymentModel' + } + }, + resources: { + 'ui:title': 'Resource Requirements', + 'ui:widget': 'hidden', + 'ui:order': ['claims', 'limits', 'requests'], + claims: { + items: { + 'ui:order': ['name', 'request'] + } + } + }, + advanced: { + 'ui:title': 'Advanced configuration', + 'ui:widget': 'hidden', + 'ui:order': ['env', 'scheduling'], + scheduling: { + 'ui:widget': 'hidden', + affinity: { + 'ui:order': ['nodeAffinity', 'podAffinity', 'podAntiAffinity'], + nodeAffinity: { + 'ui:order': [ + 'preferredDuringSchedulingIgnoredDuringExecution', + 'requiredDuringSchedulingIgnoredDuringExecution' + ], + preferredDuringSchedulingIgnoredDuringExecution: { + items: { + 'ui:order': ['preference', 'weight'], + preference: { + 'ui:order': ['matchExpressions', 'matchFields'], + matchExpressions: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + }, + matchFields: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + } + } + } + }, + requiredDuringSchedulingIgnoredDuringExecution: { + nodeSelectorTerms: { + items: { + 'ui:order': ['matchExpressions', 'matchFields'], + matchExpressions: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + }, + matchFields: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + } + } + } + } + }, + podAffinity: { + 'ui:order': [ + 'preferredDuringSchedulingIgnoredDuringExecution', + 'requiredDuringSchedulingIgnoredDuringExecution' + ], + preferredDuringSchedulingIgnoredDuringExecution: { + items: { + 'ui:order': ['podAffinityTerm', 'weight'], + podAffinityTerm: { + 'ui:order': [ + 'topologyKey', + 'labelSelector', + 'matchLabelKeys', + 'mismatchLabelKeys', + 'namespaceSelector', + 'namespaces' + ], + labelSelector: { + 'ui:order': ['matchExpressions', 'matchLabels'], + matchExpressions: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + } + }, + namespaceSelector: { + 'ui:order': ['matchExpressions', 'matchLabels'], + matchExpressions: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + } + } + } + } + }, + requiredDuringSchedulingIgnoredDuringExecution: { + items: { + 'ui:order': [ + 'topologyKey', + 'labelSelector', + 'matchLabelKeys', + 'mismatchLabelKeys', + 'namespaceSelector', + 'namespaces' + ], + labelSelector: { + 'ui:order': ['matchExpressions', 'matchLabels'], + matchExpressions: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + } + }, + namespaceSelector: { + 'ui:order': ['matchExpressions', 'matchLabels'], + matchExpressions: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + } + } + } + } + }, + podAntiAffinity: { + 'ui:order': [ + 'preferredDuringSchedulingIgnoredDuringExecution', + 'requiredDuringSchedulingIgnoredDuringExecution' + ], + preferredDuringSchedulingIgnoredDuringExecution: { + items: { + 'ui:order': ['podAffinityTerm', 'weight'], + podAffinityTerm: { + 'ui:order': [ + 'topologyKey', + 'labelSelector', + 'matchLabelKeys', + 'mismatchLabelKeys', + 'namespaceSelector', + 'namespaces' + ], + labelSelector: { + 'ui:order': ['matchExpressions', 'matchLabels'], + matchExpressions: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + } + }, + namespaceSelector: { + 'ui:order': ['matchExpressions', 'matchLabels'], + matchExpressions: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + } + } + } + } + }, + requiredDuringSchedulingIgnoredDuringExecution: { + items: { + 'ui:order': [ + 'topologyKey', + 'labelSelector', + 'matchLabelKeys', + 'mismatchLabelKeys', + 'namespaceSelector', + 'namespaces' + ], + labelSelector: { + 'ui:order': ['matchExpressions', 'matchLabels'], + matchExpressions: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + } + }, + namespaceSelector: { + 'ui:order': ['matchExpressions', 'matchLabels'], + matchExpressions: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + } + } + } + } + } + }, + tolerations: { + items: { + 'ui:order': ['effect', 'key', 'operator', 'tolerationSeconds', 'value'] + } + }, + 'ui:order': ['affinity', 'nodeSelector', 'priorityClassName', 'tolerations'] + } + }, + 'ui:order': [ + 'sampling', + 'privileged', + 'features', + 'flowFilter', + 'interfaces', + 'excludeInterfaces', + 'logLevel', + 'imagePullPolicy', + 'metrics', + 'cacheMaxFlows', + 'cacheActiveTimeout', + 'kafkaBatchSize', + 'resources', + 'advanced' + ] + }, + 'ui:order': ['ipfix', 'type', 'ebpf'] + }, + processor: { + 'ui:title': 'Processor configuration', + 'ui:description': + 'The component that receives the flows from the agent, enriches them, generates metrics, and forwards them to the Loki persistence layer and/or any available exporter.', + filters: { + 'ui:title': 'Filters', + 'ui:description': + 'Define custom filters to limit the amount of generated flows.\nThese filters provide more flexibility than the eBPF Agent filters (in `spec.agent.ebpf.flowFilter`), such as allowing to filter by Kubernetes namespace, but with a lesser improvement in performance. Unsupported.', + 'ui:widget': 'hidden', + items: { + 'ui:order': ['allOf', 'outputTarget', 'sampling'], + allOf: { + items: { + 'ui:order': ['field', 'matchType', 'value'] + } + } + } + }, + multiClusterDeployment: { + 'ui:title': 'Multi-cluster deployment', + 'ui:description': 'Enable multi clusters feature. This adds `clusterName` label to flows data' + }, + clusterName: { + 'ui:title': 'Cluster name', + 'ui:description': + 'The name of the cluster to appear in the flows data. This is useful in a multi-cluster context. When using OpenShift, leave empty to make it automatically', + 'ui:dependency': { + controlFieldPath: ['processor', 'multiClusterDeployment'], + controlFieldValue: 'true', + controlFieldName: 'multiClusterDeployment' + } + }, + addZone: { + 'ui:title': 'Availability zones', + 'ui:description': + 'Allows availability zone awareness by labelling flows with their source and destination zones.\nThis feature requires the "topology.kubernetes.io/zone" label to be set on nodes.' + }, + subnetLabels: { + 'ui:title': 'Subnet labels', + 'ui:description': + 'Allows to define custom labels on subnets and IPs or to enable automatic labelling of recognized subnets in OpenShift, which is used to identify cluster external traffic.\nWhen a subnet matches the source or destination IP of a flow, a corresponding field is added: `SrcSubnetLabel` or `DstSubnetLabel`.', + openShiftAutoDetect: { + 'ui:widget': 'hidden' + }, + customLabels: { + 'ui:title': 'Custom labels', + 'ui:description': + 'allows to customize subnets and IPs labelling, such as to identify cluster-external workloads or web services.\nIf you enable `openShiftAutoDetect`, `customLabels` can override the detected subnets in case they overlap.', + items: { + 'ui:order': ['cidrs', 'name'] + } + }, + 'ui:order': ['openShiftAutoDetect', 'customLabels'] + }, + logTypes: { + 'ui:title': 'Log types', + 'ui:widget': 'hidden' + }, + logLevel: { + 'ui:title': 'Log level', + 'ui:description': 'The log level of the processor runtime' + }, + imagePullPolicy: { + 'ui:title': 'Image pull policy', + 'ui:description': 'The Kubernetes pull policy for the image defined above' + }, + deduper: { + 'ui:title': 'Deduper', + 'ui:description': + 'Allows you to sample or drop flows identified as duplicates, in order to save on resource usage. Unsupported.', + mode: { + 'ui:title': 'Mode' + }, + sampling: { + 'ui:title': 'Sampling' + }, + 'ui:order': ['mode', 'sampling'] + }, + kafkaConsumerQueueCapacity: { + 'ui:title': 'Kafka consumer queue capacity', + 'ui:dependency': { + controlFieldPath: ['deploymentModel'], + controlFieldValue: 'Kafka', + controlFieldName: 'deploymentModel' + } + }, + kafkaConsumerAutoscaler: { + 'ui:title': 'kafka consumer autoscaler', + 'ui:dependency': { + controlFieldPath: ['deploymentModel'], + controlFieldValue: 'Kafka', + controlFieldName: 'deploymentModel' + }, + 'ui:order': ['maxReplicas', 'metrics', 'minReplicas', 'status'], + metrics: { + items: { + 'ui:order': ['type', 'containerResource', 'external', 'object', 'pods', 'resource'], + containerResource: { + 'ui:order': ['container', 'name', 'target'], + target: { + 'ui:order': ['type', 'averageUtilization', 'averageValue', 'value'] + } + }, + external: { + 'ui:order': ['metric', 'target'], + metric: { + 'ui:order': ['name', 'selector'], + selector: { + 'ui:order': ['matchExpressions', 'matchLabels'], + matchExpressions: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + } + } + }, + target: { + 'ui:order': ['type', 'averageUtilization', 'averageValue', 'value'] + } + }, + object: { + 'ui:order': ['describedObject', 'metric', 'target'], + describedObject: { + 'ui:order': ['kind', 'name', 'apiVersion'] + }, + metric: { + 'ui:order': ['name', 'selector'], + selector: { + 'ui:order': ['matchExpressions', 'matchLabels'], + matchExpressions: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + } + } + }, + target: { + 'ui:order': ['type', 'averageUtilization', 'averageValue', 'value'] + } + }, + pods: { + 'ui:order': ['metric', 'target'], + metric: { + 'ui:order': ['name', 'selector'], + selector: { + 'ui:order': ['matchExpressions', 'matchLabels'], + matchExpressions: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + } + } + }, + target: { + 'ui:order': ['type', 'averageUtilization', 'averageValue', 'value'] + } + }, + resource: { + 'ui:order': ['name', 'target'], + target: { + 'ui:order': ['type', 'averageUtilization', 'averageValue', 'value'] + } + } + } + } + }, + kafkaConsumerReplicas: { + 'ui:title': 'Kafka consumer replicas', + 'ui:dependency': { + controlFieldPath: ['deploymentModel'], + controlFieldValue: 'Kafka', + controlFieldName: 'deploymentModel' + } + }, + kafkaConsumerBatchSize: { + 'ui:title': 'Kafka consumer batch size', + 'ui:dependency': { + controlFieldPath: ['deploymentModel'], + controlFieldValue: 'Kafka', + controlFieldName: 'deploymentModel' + } + }, + metrics: { + 'ui:title': 'Metrics configuration', + 'ui:description': 'The processor configuration regarding metrics', + server: { + 'ui:title': 'Server configuration', + tls: { + 'ui:title': 'TLS configuration', + insecureSkipVerify: { + 'ui:title': 'Insecure', + 'ui:dependency': { + controlFieldPath: ['processor', 'metrics', 'server', 'tls', 'type'], + controlFieldValue: 'Provided', + controlFieldName: 'type' + } + }, + provided: { + 'ui:title': 'Cert', + 'ui:dependency': { + controlFieldPath: ['processor', 'metrics', 'server', 'tls', 'type'], + controlFieldValue: 'Provided', + controlFieldName: 'type' + }, + 'ui:order': ['certFile', 'certKey', 'name', 'namespace', 'type'] + }, + providedCaFile: { + 'ui:title': 'CA', + 'ui:dependency': { + controlFieldPath: ['processor', 'metrics', 'server', 'tls', 'type'], + controlFieldValue: 'Provided', + controlFieldName: 'type' + }, + 'ui:order': ['file', 'name', 'namespace', 'type'] + }, + 'ui:order': ['type', 'insecureSkipVerify', 'provided', 'providedCaFile'] + }, + port: { + 'ui:title': 'Port' + }, + 'ui:order': ['tls', 'port'] + }, + disableAlerts: { + 'ui:title': 'Disable alerts', + 'ui:description': + 'List of alerts that should be disabled.\nPossible values are:\n`NetObservNoFlows`, which is triggered when no flows are being observed for a certain period.\n`NetObservLokiError`, which is triggered when flows are being dropped due to Loki errors.' + }, + includeList: { + 'ui:title': 'Include list', + 'ui:description': + 'List of metric names to specify which ones to generate.\nThe names correspond to the names in Prometheus without the prefix. For example,\n`namespace_egress_packets_total` shows up as `netobserv_namespace_egress_packets_total` in Prometheus.\nNote that the more metrics you add, the bigger is the impact on Prometheus workload resources.\nMetrics enabled by default are:\n`namespace_flows_total`, `node_ingress_bytes_total`, `node_egress_bytes_total`, `workload_ingress_bytes_total`,\n`workload_egress_bytes_total`, `namespace_drop_packets_total` (when `PacketDrop` feature is enabled),\n`namespace_rtt_seconds` (when `FlowRTT` feature is enabled), `namespace_dns_latency_seconds` (when `DNSTracking` feature is enabled),\n`namespace_network_policy_events_total` (when `NetworkEvents` feature is enabled).\nMore information, with full list of available metrics: https://github.com/netobserv/network-observability-operator/blob/main/docs/Metrics.md' + }, + 'ui:order': ['server', 'disableAlerts', 'includeList'] + }, + resources: { + 'ui:title': 'Resource Requirements', + 'ui:widget': 'hidden', + 'ui:order': ['claims', 'limits', 'requests'], + claims: { + items: { + 'ui:order': ['name', 'request'] + } + } + }, + advanced: { + 'ui:title': 'Advanced configuration', + port: { + 'ui:widget': 'hidden' + }, + conversationTerminatingTimeout: { + 'ui:widget': 'hidden' + }, + conversationEndTimeout: { + 'ui:widget': 'hidden' + }, + profilePort: { + 'ui:widget': 'hidden' + }, + env: { + 'ui:widget': 'hidden' + }, + enableKubeProbes: { + 'ui:widget': 'hidden' + }, + scheduling: { + 'ui:widget': 'hidden', + 'ui:order': ['affinity', 'nodeSelector', 'priorityClassName', 'tolerations'], + affinity: { + 'ui:order': ['nodeAffinity', 'podAffinity', 'podAntiAffinity'], + nodeAffinity: { + 'ui:order': [ + 'preferredDuringSchedulingIgnoredDuringExecution', + 'requiredDuringSchedulingIgnoredDuringExecution' + ], + preferredDuringSchedulingIgnoredDuringExecution: { + items: { + 'ui:order': ['preference', 'weight'], + preference: { + 'ui:order': ['matchExpressions', 'matchFields'], + matchExpressions: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + }, + matchFields: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + } + } + } + }, + requiredDuringSchedulingIgnoredDuringExecution: { + nodeSelectorTerms: { + items: { + 'ui:order': ['matchExpressions', 'matchFields'], + matchExpressions: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + }, + matchFields: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + } + } + } + } + }, + podAffinity: { + 'ui:order': [ + 'preferredDuringSchedulingIgnoredDuringExecution', + 'requiredDuringSchedulingIgnoredDuringExecution' + ], + preferredDuringSchedulingIgnoredDuringExecution: { + items: { + 'ui:order': ['podAffinityTerm', 'weight'], + podAffinityTerm: { + 'ui:order': [ + 'topologyKey', + 'labelSelector', + 'matchLabelKeys', + 'mismatchLabelKeys', + 'namespaceSelector', + 'namespaces' + ], + labelSelector: { + 'ui:order': ['matchExpressions', 'matchLabels'], + matchExpressions: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + } + }, + namespaceSelector: { + 'ui:order': ['matchExpressions', 'matchLabels'], + matchExpressions: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + } + } + } + } + }, + requiredDuringSchedulingIgnoredDuringExecution: { + items: { + 'ui:order': [ + 'topologyKey', + 'labelSelector', + 'matchLabelKeys', + 'mismatchLabelKeys', + 'namespaceSelector', + 'namespaces' + ], + labelSelector: { + 'ui:order': ['matchExpressions', 'matchLabels'], + matchExpressions: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + } + }, + namespaceSelector: { + 'ui:order': ['matchExpressions', 'matchLabels'], + matchExpressions: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + } + } + } + } + }, + podAntiAffinity: { + 'ui:order': [ + 'preferredDuringSchedulingIgnoredDuringExecution', + 'requiredDuringSchedulingIgnoredDuringExecution' + ], + preferredDuringSchedulingIgnoredDuringExecution: { + items: { + 'ui:order': ['podAffinityTerm', 'weight'], + podAffinityTerm: { + 'ui:order': [ + 'topologyKey', + 'labelSelector', + 'matchLabelKeys', + 'mismatchLabelKeys', + 'namespaceSelector', + 'namespaces' + ], + labelSelector: { + 'ui:order': ['matchExpressions', 'matchLabels'], + matchExpressions: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + } + }, + namespaceSelector: { + 'ui:order': ['matchExpressions', 'matchLabels'], + matchExpressions: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + } + } + } + } + }, + requiredDuringSchedulingIgnoredDuringExecution: { + items: { + 'ui:order': [ + 'topologyKey', + 'labelSelector', + 'matchLabelKeys', + 'mismatchLabelKeys', + 'namespaceSelector', + 'namespaces' + ], + labelSelector: { + 'ui:order': ['matchExpressions', 'matchLabels'], + matchExpressions: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + } + }, + namespaceSelector: { + 'ui:order': ['matchExpressions', 'matchLabels'], + matchExpressions: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + } + } + } + } + } + }, + tolerations: { + items: { + 'ui:order': ['effect', 'key', 'operator', 'tolerationSeconds', 'value'] + } + } + }, + secondaryNetworks: { + 'ui:title': 'Secondary networks', + items: { + name: { + 'ui:title': 'Name' + }, + index: { + 'ui:title': 'Index', + 'ui:widget': 'arrayCheckboxes' + }, + 'ui:order': ['name', 'index'] + } + }, + healthPort: { + 'ui:widget': 'hidden' + }, + dropUnusedFields: { + 'ui:widget': 'hidden' + }, + conversationHeartbeatInterval: { + 'ui:widget': 'hidden' + }, + 'ui:order': [ + 'port', + 'conversationTerminatingTimeout', + 'conversationEndTimeout', + 'profilePort', + 'env', + 'enableKubeProbes', + 'scheduling', + 'secondaryNetworks', + 'healthPort', + 'dropUnusedFields', + 'conversationHeartbeatInterval' + ] + }, + 'ui:order': [ + 'filters', + 'multiClusterDeployment', + 'clusterName', + 'addZone', + 'subnetLabels', + 'logTypes', + 'logLevel', + 'imagePullPolicy', + 'deduper', + 'kafkaConsumerReplicas', + 'kafkaConsumerAutoscaler', + 'kafkaConsumerQueueCapacity', + 'kafkaConsumerBatchSize', + 'metrics', + 'resources', + 'advanced' + ] + }, + prometheus: { + 'ui:title': 'Prometheus', + 'ui:flat': 'true', + querier: { + 'ui:title': 'Prometheus querier configuration', + enable: { + 'ui:title': 'Use Prometheus storage', + 'ui:description': + 'When enabled, the Console plugin queries flow metrics from Prometheus instead of Loki whenever possible.\nIt is enbaled by default: set it to `false` to disable this feature.\nThe Console plugin can use either Loki or Prometheus as a data source for metrics (see also `spec.loki`), or both.\nNot all queries are transposable from Loki to Prometheus. Hence, if Loki is disabled, some features of the plugin are disabled as well,\nsuch as getting per-pod information or viewing raw flows.\nIf both Prometheus and Loki are enabled, Prometheus takes precedence and Loki is used as a fallback for queries that Prometheus cannot handle.\nIf they are both disabled, the Console plugin is not deployed.' + }, + mode: { + 'ui:title': 'Mode', + 'ui:description': + 'Must be set according to the type of Prometheus installation that stores network observability metrics:\n- Use `Auto` to try configuring automatically. In OpenShift, it uses the Thanos querier from OpenShift Cluster Monitoring\n- Use `Manual` for a manual setup' + }, + manual: { + 'ui:title': 'Manual', + 'ui:description': 'Prometheus configuration for manual mode.', + 'ui:flat': 'true', + 'ui:dependency': { + controlFieldPath: ['prometheus', 'querier', 'mode'], + controlFieldValue: 'Manual', + controlFieldName: 'mode' + }, + forwardUserToken: { + 'ui:title': 'Forward user token' + }, + url: { + 'ui:title': 'Url' + }, + tls: { + 'ui:title': 'TLS configuration', + enable: { + 'ui:title': 'Use TLS' + }, + caCert: { + 'ui:title': 'CA certificate', + 'ui:order': ['certFile', 'certKey', 'name', 'namespace', 'type'] + }, + userCert: { + 'ui:title': 'User certificate when using mTLS', + 'ui:order': ['certFile', 'certKey', 'name', 'namespace', 'type'] + }, + insecureSkipVerify: { + 'ui:title': 'Insecure skip verify' + }, + 'ui:order': ['enable', 'caCert', 'userCert', 'insecureSkipVerify'] + }, + 'ui:order': ['forwardUserToken', 'url', 'tls'] + }, + timeout: { + 'ui:title': 'Timeout', + 'ui:description': + 'The read timeout for console plugin queries to Prometheus.\nA timeout of zero means no timeout.' + }, + 'ui:order': ['enable', 'mode', 'manual', 'timeout'] + } + }, + loki: { + 'ui:title': 'Loki client settings', + 'ui:description': 'Flow logs storage.', + enable: { + 'ui:title': 'Use Loki storage' + }, + mode: { + 'ui:title': 'Mode' + }, + manual: { + 'ui:title': 'Manual', + 'ui:flat': 'true', + 'ui:dependency': { + controlFieldPath: ['loki', 'mode'], + controlFieldValue: 'Manual', + controlFieldName: 'mode' + }, + authToken: { + 'ui:title': 'Auth token' + }, + ingesterUrl: { + 'ui:title': 'Ingester url' + }, + querierUrl: { + 'ui:title': 'Querier url' + }, + statusUrl: { + 'ui:title': 'Status url' + }, + tenantID: { + 'ui:title': 'Tenant id' + }, + 'ui:order': ['authToken', 'ingesterUrl', 'querierUrl', 'statusUrl', 'tenantID', 'statusTls', 'tls'], + statusTls: { + 'ui:title': 'TLS configuration', + enable: { + 'ui:title': 'Use TLS' + }, + caCert: { + 'ui:title': 'CA certificate', + 'ui:order': ['certFile', 'certKey', 'name', 'namespace', 'type'] + }, + userCert: { + 'ui:title': 'User certificate when using mTLS', + 'ui:order': ['certFile', 'certKey', 'name', 'namespace', 'type'] + }, + insecureSkipVerify: { + 'ui:title': 'Insecure skip verify' + }, + 'ui:order': ['enable', 'caCert', 'userCert', 'insecureSkipVerify'] + }, + tls: { + 'ui:title': 'TLS configuration', + enable: { + 'ui:title': 'Use TLS' + }, + caCert: { + 'ui:title': 'CA certificate', + 'ui:order': ['certFile', 'certKey', 'name', 'namespace', 'type'] + }, + userCert: { + 'ui:title': 'User certificate when using mTLS', + 'ui:order': ['certFile', 'certKey', 'name', 'namespace', 'type'] + }, + insecureSkipVerify: { + 'ui:title': 'Insecure skip verify' + }, + 'ui:order': ['enable', 'caCert', 'userCert', 'insecureSkipVerify'] + } + }, + monolithic: { + 'ui:title': 'Monolithic', + 'ui:flat': 'true', + 'ui:dependency': { + controlFieldPath: ['loki', 'mode'], + controlFieldValue: 'Monolithic', + controlFieldName: 'mode' + }, + tenantID: { + 'ui:title': 'Tenant id' + }, + url: { + 'ui:title': 'Url' + }, + 'ui:order': ['tenantID', 'url', 'tls'], + tls: { + 'ui:title': 'TLS configuration', + enable: { + 'ui:title': 'Use TLS' + }, + caCert: { + 'ui:title': 'CA certificate', + 'ui:order': ['certFile', 'certKey', 'name', 'namespace', 'type'] + }, + userCert: { + 'ui:title': 'User certificate when using mTLS', + 'ui:order': ['certFile', 'certKey', 'name', 'namespace', 'type'] + }, + insecureSkipVerify: { + 'ui:title': 'Insecure skip verify' + }, + 'ui:order': ['enable', 'caCert', 'userCert', 'insecureSkipVerify'] + } + }, + microservices: { + 'ui:title': 'Microservices', + 'ui:flat': 'true', + 'ui:dependency': { + controlFieldPath: ['loki', 'mode'], + controlFieldValue: 'Microservices', + controlFieldName: 'mode' + }, + ingesterUrl: { + 'ui:title': 'Ingester url' + }, + querierUrl: { + 'ui:title': 'Querier url' + }, + tenantID: { + 'ui:title': 'Tenant id' + }, + 'ui:order': ['ingesterUrl', 'querierUrl', 'tenantID', 'tls'], + tls: { + 'ui:title': 'TLS configuration', + enable: { + 'ui:title': 'Use TLS' + }, + caCert: { + 'ui:title': 'CA certificate', + 'ui:order': ['certFile', 'certKey', 'name', 'namespace', 'type'] + }, + userCert: { + 'ui:title': 'User certificate when using mTLS', + 'ui:order': ['certFile', 'certKey', 'name', 'namespace', 'type'] + }, + insecureSkipVerify: { + 'ui:title': 'Insecure skip verify' + }, + 'ui:order': ['enable', 'caCert', 'userCert', 'insecureSkipVerify'] + } + }, + lokiStack: { + 'ui:title': 'Loki stack', + 'ui:flat': 'true', + 'ui:dependency': { + controlFieldPath: ['loki', 'mode'], + controlFieldValue: 'LokiStack', + controlFieldName: 'mode' + }, + name: { + 'ui:title': 'Name' + }, + namespace: { + 'ui:title': 'Namespace' + }, + 'ui:order': ['name', 'namespace'] + }, + readTimeout: { + 'ui:title': 'Read timeout', + 'ui:description': + 'The maximum console plugin loki query total time limit.\nA timeout of zero means no timeout.', + 'ui:widget': 'hidden' + }, + writeTimeout: { + 'ui:title': 'Write timeout', + 'ui:description': 'The maximum Loki time connection / request limit.\nA timeout of zero means no timeout.', + 'ui:widget': 'hidden' + }, + writeBatchWait: { + 'ui:title': 'Write batch wait', + 'ui:description': 'The maximum time to wait before sending a Loki batch.', + 'ui:widget': 'hidden' + }, + writeBatchSize: { + 'ui:title': 'Write batch size', + 'ui:description': 'The maximum batch size (in bytes) of Loki logs to accumulate before sending.', + 'ui:widget': 'hidden' + }, + advanced: { + 'ui:title': 'Advanced configuration', + 'ui:description': + 'Internal configuration of the Loki clients.\nThis section is aimed mostly for debugging and fine-grained performance optimizations.', + 'ui:widget': 'hidden', + staticLabels: { + 'ui:title': 'Static labels', + 'ui:description': 'A map of common labels to set on each flow in Loki storage.' + }, + writeMaxRetries: { + 'ui:title': 'Write max retries', + 'ui:description': 'The maximum number of retries for Loki client connections.' + }, + writeMaxBackoff: { + 'ui:title': 'Write max backoff', + 'ui:description': 'The maximum backoff time for Loki client connection between retries.' + }, + writeMinBackoff: { + 'ui:title': 'Write min backoff', + 'ui:description': 'The initial backoff time for Loki client connection between retries.' + }, + 'ui:order': ['staticLabels', 'writeMaxRetries', 'writeMaxBackoff', 'writeMinBackoff'] + }, + 'ui:order': [ + 'enable', + 'mode', + 'manual', + 'monolithic', + 'microservices', + 'lokiStack', + 'readTimeout', + 'writeTimeout', + 'writeBatchWait', + 'writeBatchSize', + 'advanced' + ] + }, + consolePlugin: { + 'ui:title': 'Console plugin configuration', + 'ui:description': 'The OpenShift Console integration.', + enable: { + 'ui:title': 'Deploy console plugin' + }, + logLevel: { + 'ui:title': 'Log level', + 'ui:description': 'Log level for the console plugin backend' + }, + imagePullPolicy: { + 'ui:title': 'Image pull policy', + 'ui:description': 'The Kubernetes pull policy for the image defined above' + }, + portNaming: { + 'ui:title': 'Port naming', + 'ui:description': 'The configuration of the port-to-service name translation', + 'ui:widget': 'hidden', + enable: { + 'ui:title': 'Enable' + }, + portNames: { + 'ui:title': 'Port names' + }, + 'ui:order': ['enable', 'portNames'] + }, + resources: { + 'ui:title': 'Resource Requirements', + 'ui:widget': 'hidden', + 'ui:order': ['claims', 'limits', 'requests'], + claims: { + items: { + 'ui:order': ['name', 'request'] + } + } + }, + quickFilters: { + 'ui:title': 'Quick filters', + 'ui:description': 'Configure quick filter presets for the Console plugin', + 'ui:widget': 'hidden', + items: { + 'ui:order': ['filter', 'name', 'default'] + } + }, + replicas: { + 'ui:title': 'Replicas', + 'ui:description': 'The number of replicas (pods) to start.' + }, + autoscaler: { + 'ui:title': 'Horizontal pod autoscaler', + 'ui:widget': 'hidden', + 'ui:order': ['maxReplicas', 'metrics', 'minReplicas', 'status'], + metrics: { + items: { + 'ui:order': ['type', 'containerResource', 'external', 'object', 'pods', 'resource'], + containerResource: { + 'ui:order': ['container', 'name', 'target'], + target: { + 'ui:order': ['type', 'averageUtilization', 'averageValue', 'value'] + } + }, + external: { + 'ui:order': ['metric', 'target'], + metric: { + 'ui:order': ['name', 'selector'], + selector: { + 'ui:order': ['matchExpressions', 'matchLabels'], + matchExpressions: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + } + } + }, + target: { + 'ui:order': ['type', 'averageUtilization', 'averageValue', 'value'] + } + }, + object: { + 'ui:order': ['describedObject', 'metric', 'target'], + describedObject: { + 'ui:order': ['kind', 'name', 'apiVersion'] + }, + metric: { + 'ui:order': ['name', 'selector'], + selector: { + 'ui:order': ['matchExpressions', 'matchLabels'], + matchExpressions: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + } + } + }, + target: { + 'ui:order': ['type', 'averageUtilization', 'averageValue', 'value'] + } + }, + pods: { + 'ui:order': ['metric', 'target'], + metric: { + 'ui:order': ['name', 'selector'], + selector: { + 'ui:order': ['matchExpressions', 'matchLabels'], + matchExpressions: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + } + } + }, + target: { + 'ui:order': ['type', 'averageUtilization', 'averageValue', 'value'] + } + }, + resource: { + 'ui:order': ['name', 'target'], + target: { + 'ui:order': ['type', 'averageUtilization', 'averageValue', 'value'] + } + } + } + } + }, + advanced: { + 'ui:title': 'Advanced configuration', + 'ui:description': + 'Internal configuration of the console plugin.\nThis section is aimed mostly for debugging and fine-grained performance optimizations, such as `GOGC` and `GOMAXPROCS` env vars. Set these values at your own risk.', + 'ui:widget': 'hidden', + 'ui:order': ['args', 'env', 'port', 'register', 'scheduling'], + scheduling: { + 'ui:widget': 'hidden', + affinity: { + 'ui:order': ['nodeAffinity', 'podAffinity', 'podAntiAffinity'], + nodeAffinity: { + 'ui:order': [ + 'preferredDuringSchedulingIgnoredDuringExecution', + 'requiredDuringSchedulingIgnoredDuringExecution' + ], + preferredDuringSchedulingIgnoredDuringExecution: { + items: { + 'ui:order': ['preference', 'weight'], + preference: { + 'ui:order': ['matchExpressions', 'matchFields'], + matchExpressions: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + }, + matchFields: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + } + } + } + }, + requiredDuringSchedulingIgnoredDuringExecution: { + nodeSelectorTerms: { + items: { + 'ui:order': ['matchExpressions', 'matchFields'], + matchExpressions: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + }, + matchFields: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + } + } + } + } + }, + podAffinity: { + 'ui:order': [ + 'preferredDuringSchedulingIgnoredDuringExecution', + 'requiredDuringSchedulingIgnoredDuringExecution' + ], + preferredDuringSchedulingIgnoredDuringExecution: { + items: { + 'ui:order': ['podAffinityTerm', 'weight'], + podAffinityTerm: { + 'ui:order': [ + 'topologyKey', + 'labelSelector', + 'matchLabelKeys', + 'mismatchLabelKeys', + 'namespaceSelector', + 'namespaces' + ], + labelSelector: { + 'ui:order': ['matchExpressions', 'matchLabels'], + matchExpressions: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + } + }, + namespaceSelector: { + 'ui:order': ['matchExpressions', 'matchLabels'], + matchExpressions: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + } + } + } + } + }, + requiredDuringSchedulingIgnoredDuringExecution: { + items: { + 'ui:order': [ + 'topologyKey', + 'labelSelector', + 'matchLabelKeys', + 'mismatchLabelKeys', + 'namespaceSelector', + 'namespaces' + ], + labelSelector: { + 'ui:order': ['matchExpressions', 'matchLabels'], + matchExpressions: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + } + }, + namespaceSelector: { + 'ui:order': ['matchExpressions', 'matchLabels'], + matchExpressions: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + } + } + } + } + }, + podAntiAffinity: { + 'ui:order': [ + 'preferredDuringSchedulingIgnoredDuringExecution', + 'requiredDuringSchedulingIgnoredDuringExecution' + ], + preferredDuringSchedulingIgnoredDuringExecution: { + items: { + 'ui:order': ['podAffinityTerm', 'weight'], + podAffinityTerm: { + 'ui:order': [ + 'topologyKey', + 'labelSelector', + 'matchLabelKeys', + 'mismatchLabelKeys', + 'namespaceSelector', + 'namespaces' + ], + labelSelector: { + 'ui:order': ['matchExpressions', 'matchLabels'], + matchExpressions: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + } + }, + namespaceSelector: { + 'ui:order': ['matchExpressions', 'matchLabels'], + matchExpressions: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + } + } + } + } + }, + requiredDuringSchedulingIgnoredDuringExecution: { + items: { + 'ui:order': [ + 'topologyKey', + 'labelSelector', + 'matchLabelKeys', + 'mismatchLabelKeys', + 'namespaceSelector', + 'namespaces' + ], + labelSelector: { + 'ui:order': ['matchExpressions', 'matchLabels'], + matchExpressions: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + } + }, + namespaceSelector: { + 'ui:order': ['matchExpressions', 'matchLabels'], + matchExpressions: { + items: { + 'ui:order': ['key', 'operator', 'values'] + } + } + } + } + } + } + }, + tolerations: { + items: { + 'ui:order': ['effect', 'key', 'operator', 'tolerationSeconds', 'value'] + } + }, + 'ui:order': ['affinity', 'nodeSelector', 'priorityClassName', 'tolerations'] + } + }, + 'ui:order': [ + 'enable', + 'logLevel', + 'imagePullPolicy', + 'portNaming', + 'quickFilters', + 'replicas', + 'autoscaler', + 'resources', + 'advanced' + ] + }, + networkPolicy: { + 'ui:title': 'Network policy', + 'ui:description': 'Ingress network policy settings for network observability components isolation.', + enable: { + 'ui:title': 'Deploy policies' + }, + additionalNamespaces: { + 'ui:title': 'Additional namespaces', + 'ui:description': + 'Namespaces allowed to connect to the network observability namespace.\nIt provides flexibility in the network policy configuration, but if you need a more specific\nconfiguration, you can disable it and install your own instead.', + 'ui:dependency': { + controlFieldPath: ['networkPolicy', 'enable'], + controlFieldValue: 'true', + controlFieldName: 'enable' + } + }, + 'ui:order': ['enable', 'additionalNamespaces'] + }, + exporters: { + 'ui:title': 'Exporters', + 'ui:description': 'Additional optional exporters for custom consumption or storage.', + items: { + type: { + 'ui:title': 'Type', + 'ui:description': 'Type of exporters. The available options are `Kafka`, `IPFIX`, and `OpenTelemetry`.' + }, + ipfix: { + 'ui:title': 'IPFIX configuration', + 'ui:flat': 'true', + 'ui:dependency': { + controlFieldPath: ['exporters', 'type'], + controlFieldValue: 'IPFIX', + controlFieldName: 'type' + }, + targetHost: { + 'ui:title': 'Target host' + }, + targetPort: { + 'ui:title': 'Target port' + }, + transport: { + 'ui:title': 'Transport' + }, + 'ui:order': ['targetHost', 'targetPort', 'transport'] + }, + kafka: { + 'ui:title': 'Kafka configuration', + 'ui:flat': 'true', + 'ui:dependency': { + controlFieldPath: ['exporters', 'type'], + controlFieldValue: 'Kafka', + controlFieldName: 'type' + }, + address: { + 'ui:title': 'Address' + }, + topic: { + 'ui:title': 'Topic' + }, + tls: { + 'ui:title': 'TLS configuration', + enable: { + 'ui:title': 'Use TLS' + }, + caCert: { + 'ui:title': 'CA certificate', + 'ui:order': ['certFile', 'certKey', 'name', 'namespace', 'type'] + }, + userCert: { + 'ui:title': 'User certificate when using mTLS', + 'ui:order': ['certFile', 'certKey', 'name', 'namespace', 'type'] + }, + insecureSkipVerify: { + 'ui:title': 'Insecure skip verify' + }, + 'ui:order': ['enable', 'caCert', 'userCert', 'insecureSkipVerify'] + }, + sasl: { + 'ui:title': 'SASL', + 'ui:description': 'SASL authentication configuration. Unsupported.', + type: { + 'ui:title': 'Type' + }, + clientIDReference: { + 'ui:title': 'Client ID reference', + 'ui:order': ['file', 'name', 'namespace', 'type'] + }, + clientSecretReference: { + 'ui:title': 'Client secret reference', + 'ui:order': ['file', 'name', 'namespace', 'type'] + }, + 'ui:order': ['type', 'clientIDReference', 'clientSecretReference'] + }, + 'ui:order': ['address', 'topic', 'tls', 'sasl'] + }, + openTelemetry: { + 'ui:title': 'OpenTelemetry configuration', + 'ui:flat': 'true', + 'ui:dependency': { + controlFieldPath: ['exporters', 'type'], + controlFieldValue: 'OpenTelemetry', + controlFieldName: 'type' + }, + targetHost: { + 'ui:title': 'Target host' + }, + targetPort: { + 'ui:title': 'Target port' + }, + protocol: { + 'ui:title': 'Protocol' + }, + fieldsMapping: { + 'ui:title': 'Fields mapping', + items: { + input: { + 'ui:title': 'Input field' + }, + multiplier: { + 'ui:title': 'Multiplier' + }, + output: { + 'ui:title': 'Output field' + }, + 'ui:order': ['input', 'multiplier', 'output'] + } + }, + metrics: { + 'ui:title': 'Metrics', + enable: { + 'ui:title': 'Enable' + }, + pushTimeInterval: { + 'ui:title': 'Push time interval' + }, + 'ui:order': ['enable', 'pushTimeInterval'] + }, + tls: { + 'ui:title': 'TLS configuration', + enable: { + 'ui:title': 'Use TLS' + }, + caCert: { + 'ui:title': 'CA certificate', + 'ui:order': ['certFile', 'certKey', 'name', 'namespace', 'type'] + }, + userCert: { + 'ui:title': 'User certificate when using mTLS', + 'ui:order': ['certFile', 'certKey', 'name', 'namespace', 'type'] + }, + insecureSkipVerify: { + 'ui:title': 'Insecure skip verify' + }, + 'ui:order': ['enable', 'caCert', 'userCert', 'insecureSkipVerify'] + }, + 'ui:order': ['targetHost', 'targetPort', 'protocol', 'fieldsMapping', 'headers', 'logs', 'metrics', 'tls'] + }, + 'ui:order': ['type', 'ipfix', 'kafka', 'openTelemetry'] + } + }, + 'ui:order': [ + 'namespace', + 'deploymentModel', + 'kafka', + 'agent', + 'processor', + 'prometheus', + 'loki', + 'consolePlugin', + 'networkPolicy', + 'exporters' + ] + }, + 'ui:order': ['metadata', 'spec', '*'] +}; + +export const FlowMetricUISchema: UiSchema = { + 'ui:title': 'FlowMetric', + 'ui:description': 'The API allowing to create custom metrics from the collected flow logs.', + 'ui:flat': 'true', + metadata: { + 'ui:title': 'Metadata', + 'ui:flat': 'true', + name: { + 'ui:title': 'Name' + }, + namespace: { + 'ui:title': 'Namespace' + }, + labels: { + 'ui:widget': 'hidden' + }, + 'ui:order': ['name', 'namespace', 'labels', '*'] + }, + spec: { + metricName: { + 'ui:title': 'Metric name', + 'ui:description': 'Name of the metric. In Prometheus, it is automatically prefixed with "netobserv_".' + }, + type: { + 'ui:title': 'Type', + 'ui:description': + 'Metric type: "Counter" or "Histogram".\nUse "Counter" for any value that increases over time and on which you can compute a rate, such as Bytes or Packets.\nUse "Histogram" for any value that must be sampled independently, such as latencies.' + }, + buckets: { + 'ui:title': 'Buckets', + 'ui:description': + 'A list of buckets to use when `type` is "Histogram". The list must be parsable as floats. When not set, Prometheus default buckets are used.', + 'ui:dependency': { + controlFieldPath: ['type'], + controlFieldValue: 'Histogram', + controlFieldName: 'type' + } + }, + valueField: { + 'ui:title': 'Value field', + 'ui:description': + 'Flow field that must be used as a value for this metric. This field must hold numeric values.\nLeave empty to count flows rather than a specific value per flow.\nRefer to the documentation for the list of available fields: https://docs.openshift.com/container-platform/latest/observability/network_observability/json-flows-format-reference.html.' + }, + divider: { + 'ui:title': 'Divider', + 'ui:description': 'When nonzero, scale factor (divider) of the value. Metric value = Flow value / Divider.' + }, + labels: { + 'ui:title': 'Labels', + 'ui:description': + 'List of fields that should be used as Prometheus labels, also known as dimensions.\nFrom choosing labels results the level of granularity of this metric, and the available aggregations at query time.\nIt must be done carefully as it impacts the metric cardinality (cf https://rhobs-handbook.netlify.app/products/openshiftmonitoring/telemetry.md/#what-is-the-cardinality-of-a-metric).\nIn general, avoid setting very high cardinality labels such as IP or MAC addresses.\n"SrcK8S_OwnerName" or "DstK8S_OwnerName" should be preferred over "SrcK8S_Name" or "DstK8S_Name" as much as possible.\nRefer to the documentation for the list of available fields: https://docs.openshift.com/container-platform/latest/observability/network_observability/json-flows-format-reference.html.' + }, + flatten: { + 'ui:title': 'Flatten', + 'ui:description': + 'List of array-type fields that must be flattened, such as Interfaces or NetworkEvents. Flattened fields generate one metric per item in that field.\nFor instance, when flattening `Interfaces` on a bytes counter, a flow having Interfaces [br-ex, ens5] increases one counter for `br-ex` and another for `ens5`.' + }, + remap: { + 'ui:title': 'Remap', + 'ui:description': + 'Use different names for the generated metric labels than the flow fields. Use the origin flow fields as keys, and the desired label names as values.', + 'ui:widget': 'map' + }, + direction: { + 'ui:title': 'Direction', + 'ui:description': + 'Filter for ingress, egress or any direction flows.\nWhen set to `Ingress`, it is equivalent to adding the regular expression filter on `FlowDirection`: `0|2`.\nWhen set to `Egress`, it is equivalent to adding the regular expression filter on `FlowDirection`: `1|2`.' + }, + filters: { + 'ui:title': 'Filters', + 'ui:description': + 'A list of fields and values used to restrict which flows are taken into account. Oftentimes, these filters must\nbe used to eliminate duplicates: `Duplicate != "true"` and `FlowDirection = "0"`.\nRefer to the documentation for the list of available fields: https://docs.openshift.com/container-platform/latest/observability/network_observability/json-flows-format-reference.html.', + items: { + field: { + 'ui:title': 'Field', + 'ui:description': 'Name of the field to filter on' + }, + matchType: { + 'ui:title': 'Match type', + 'ui:description': 'Type of matching to apply' + }, + value: { + 'ui:title': 'Value', + 'ui:description': + 'Value to filter on. When `matchType` is `Equal` or `NotEqual`, you can use field injection with `$(SomeField)` to refer to any other field of the flow.' + }, + 'ui:order': ['field', 'matchType', 'value'] + } + }, + charts: { + 'ui:title': 'Charts', + 'ui:description': 'Charts configuration, for the OpenShift Console in the administrator view, Dashboards menu.', + items: { + dashboardName: { + 'ui:title': 'Dashboard name', + 'ui:description': + 'Name of the containing dashboard. If this name does not refer to an existing dashboard, a new dashboard is created.' + }, + sectionName: { + 'ui:title': 'Section name', + 'ui:description': + 'Name of the containing dashboard section. If this name does not refer to an existing section, a new section is created.\nIf `sectionName` is omitted or empty, the chart is placed in the global top section.' + }, + title: { + 'ui:title': 'Title', + 'ui:description': 'Title of the chart.' + }, + unit: { + 'ui:title': 'Unit', + 'ui:description': + 'Unit of this chart. Only a few units are currently supported. Leave empty to use generic number.' + }, + type: { + 'ui:title': 'Type', + 'ui:description': 'Type of the chart.' + }, + queries: { + 'ui:title': 'Queries', + 'ui:description': + 'List of queries to be displayed on this chart. If `type` is `SingleStat` and multiple queries are provided,\nthis chart is automatically expanded in several panels (one per query).', + items: { + promQL: { + 'ui:title': 'Query', + 'ui:description': + 'The `promQL` query to be run against Prometheus. If the chart `type` is `SingleStat`, this query should only return\na single timeseries. For other types, a top 7 is displayed.\nYou can use `$METRIC` to refer to the metric defined in this resource. For example: `sum(rate($METRIC[2m]))`.\nTo learn more about `promQL`, refer to the Prometheus documentation: https://prometheus.io/docs/prometheus/latest/querying/basics/' + }, + legend: { + 'ui:title': 'Legend', + 'ui:description': + 'The query legend that applies to each timeseries represented in this chart. When multiple timeseries are displayed, you should set a legend\nthat distinguishes each of them. It can be done with the following format: `{{ Label }}`. For example, if the `promQL` groups timeseries per\nlabel such as: `sum(rate($METRIC[2m])) by (Label1, Label2)`, you may write as the legend: `Label1={{ Label1 }}, Label2={{ Label2 }}`.' + }, + top: { + 'ui:title': 'Top', + 'ui:description': 'Top N series to display per timestamp. Does not apply to `SingleStat` chart type.' + }, + 'ui:order': ['promQL', 'legend', 'top'] + } + }, + 'ui:order': ['dashboardName', 'sectionName', 'title', 'unit', 'type', 'queries'] + } + }, + 'ui:order': [ + 'metricName', + 'type', + 'buckets', + 'valueField', + 'divider', + 'flatten', + 'remap', + 'direction', + 'labels', + 'filters', + 'charts' + ] + }, + 'ui:order': ['metadata', 'spec', '*'] +}; diff --git a/web/src/components/forms/config/validator.ts b/web/src/components/forms/config/validator.ts new file mode 100644 index 000000000..38c6d1e9d --- /dev/null +++ b/web/src/components/forms/config/validator.ts @@ -0,0 +1,8 @@ +import { customizeValidator } from '@rjsf/validator-ajv8'; + +export const SchemaValidator = customizeValidator({ + customFormats: { + 'not-empty': /.*\S.*/, + 'k8s-name': /^[a-z0-9\-]+$/ + } +}); diff --git a/web/src/components/forms/consumption.tsx b/web/src/components/forms/consumption.tsx new file mode 100644 index 000000000..9be268db6 --- /dev/null +++ b/web/src/components/forms/consumption.tsx @@ -0,0 +1,193 @@ +import { + K8sResourceKind, + PrometheusEndpoint, + PrometheusResponse, + usePrometheusPoll +} from '@openshift-console/dynamic-plugin-sdk'; +import { Flex, FlexItem, Spinner, Text, TextVariants } from '@patternfly/react-core'; +import { WarningTriangleIcon } from '@patternfly/react-icons'; +import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; +import _ from 'lodash'; +import React, { FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import './forms.css'; + +export type ResourceCalculatorProps = { + flowCollector: K8sResourceKind | null; + setSampling?: (sampling: number) => void; +}; + +export const Consumption: FC = ({ flowCollector, setSampling }) => { + const { t } = useTranslation('plugin__netobserv-plugin'); + + const [receivedPackets, rpLoaded, rpError] = usePrometheusPoll({ + endpoint: PrometheusEndpoint.QUERY, + query: `sort_desc(sum(irate(container_network_receive_packets_total{cluster="",namespace=~".+"}[4h])) by (node,namespace,pod))` + }); + + const [transmittedPackets, tpLoaded, tpError] = usePrometheusPoll({ + endpoint: PrometheusEndpoint.QUERY, + query: `sort_desc(sum(irate(container_network_transmit_packets_total{cluster="",namespace=~".+"}[4h])) by (node,namespace,pod))` + }); + + const getCurrentSampling = React.useCallback(() => { + return flowCollector?.spec?.agent?.ebpf?.sampling || 50; + }, [flowCollector?.spec?.agent?.ebpf?.sampling]); + + const getSamplings = React.useCallback(() => { + const current = getCurrentSampling(); + let samplings = [1, 25, 50, 100, 125, 150]; + if (!samplings.includes(current)) { + samplings.push(current); + samplings = _.sortBy(samplings); + } + return samplings; + }, [getCurrentSampling]); + + const loadingComponent = () => ; + + const errorComponent = () => ; + + const value = (response?: PrometheusResponse) => { + if (!response) { + return 0; + } + return _.sumBy(response.data.result, r => Number(r.value![1])); + }; + + const labelsCount = React.useCallback( + (label: string) => { + if (!rpLoaded) { + return loadingComponent(); + } else if (rpError) { + return errorComponent(); + } else if (!receivedPackets) { + return t('n/a'); + } + return _.uniq(_.map(receivedPackets.data.result, r => r.metric[label])).length; + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [receivedPackets, rpError, rpLoaded] + ); + + const getEstimation = React.useCallback( + (sampling: number) => { + // eslint-disable-next-line max-len + // taken from https://docs.redhat.com/en/documentation/openshift_container_platform/4.18/html/network_observability/configuring-network-observability-operators#network-observability-total-resource-usage-table_network_observability + + // TODO: rely on more than nodes here + const nodes = labelsCount('node'); + const estimatedCPU = nodes <= 25 ? -0.0096 * sampling + 1.8296 : -0.1347 * sampling + 12.1247; + const estimatedMemory = nodes <= 25 ? -0.1224 * sampling + 22.1224 : -0.4898 * sampling + 87.4898; + return { + cpu: estimatedCPU > 0 ? estimatedCPU.toFixed(2) : '< 0.1', + memory: estimatedMemory > 0 ? estimatedMemory.toFixed(0) : '< 1' + }; + }, + [labelsCount] + ); + + const getRecommendations = React.useCallback(() => { + // eslint-disable-next-line max-len + // taken from https://docs.redhat.com/en/documentation/openshift_container_platform/4.18/html/network_observability/configuring-network-observability-operators + const nodes = labelsCount('node'); + return [ + { + cpu: nodes <= 10 ? 4 : 16, + memory: nodes <= 10 ? 16 : 64, + lokistackSize: nodes <= 10 ? '1x.extra-small' : nodes <= 25 ? '1x.small' : '1x.medium', + kafka: nodes <= 25 ? '6 consumers' : '18 consumers' + } + ]; + }, [labelsCount]); + + return ( + + + {t('Cluster metrics')} + + + + + + + + + + + + + + + + + +
{t('Bandwidth')}{t('Nodes')}{t('Namespaces')}{t('Pods')}
+ {!rpLoaded || !tpLoaded + ? loadingComponent() + : rpError || tpError + ? errorComponent() + : `${Math.round(value(receivedPackets) + value(transmittedPackets))} pps`} + {labelsCount('node')}{labelsCount('namespace')}{labelsCount('pod')}
+
+ + {t('Recommendations')} + + + + + + + + + + + {getRecommendations().map((reco, i) => { + return ( + + + + + + + ); + })} + +
{t('vCPU')}{t('Memory')}{t('LokiStack size')}{t('Kafka')}
{`${reco.cpu}vCPUs`}{`${reco.memory}GiB`}{reco.lokistackSize}{reco.kafka}
+
+ + {t('Estimation')} + + + + + + + + + + {getSamplings().map((sampling, i) => { + const current = getCurrentSampling() === sampling; + const estimate = getEstimation(sampling); + return ( + setSampling && setSampling(sampling)} + > + + + + + ); + })} + +
{t('Sampling')}{t('vCPU')}{t('Memory')}
{`${sampling} ${current ? t('(current)') : ''}`}{`${estimate.cpu}vCPUs`}{`${estimate.memory}GiB`}
+
+
+ ); +}; + +export default Consumption; diff --git a/web/src/components/forms/dynamic-form/const.ts b/web/src/components/forms/dynamic-form/const.ts new file mode 100644 index 000000000..6f0db3305 --- /dev/null +++ b/web/src/components/forms/dynamic-form/const.ts @@ -0,0 +1,33 @@ +export const K8S_UI_SCHEMA = { + apiVersion: { + 'ui:widget': 'hidden', + 'ui:options': { + label: false + } + }, + kind: { + 'ui:widget': 'hidden', + 'ui:options': { + label: false + } + }, + spec: { + 'ui:options': { + label: false + } + }, + status: { + 'ui:widget': 'hidden', + 'ui:options': { + label: false + } + }, + 'ui:order': ['metadata', 'spec', '*'] +}; + +export const JSON_SCHEMA_GROUP_TYPES: string[] = ['object', 'array']; +export const JSON_SCHEMA_NUMBER_TYPES: string[] = ['number', 'integer']; + +export const THOUSAND = 10 ** 3; +export const MILLION = 10 ** 6; +export const BILLION = 10 ** 9; diff --git a/web/src/components/forms/dynamic-form/dynamic-form.css b/web/src/components/forms/dynamic-form/dynamic-form.css new file mode 100644 index 000000000..a20ecba1c --- /dev/null +++ b/web/src/components/forms/dynamic-form/dynamic-form.css @@ -0,0 +1,39 @@ +.dynamic-form { + margin-left: 1.5rem; + margin-right: 1.5rem; +} + +.description { + font-size: small !important; + padding: 0; +} + +.description.padding { + padding-left: 1rem !important; +} + +.description.border { + border-left: solid; + border-width: 3px; +} + +.description.border.light { + border-left-color: #06c !important; +} + +.description.border.dark { + border-left-color: #1fa7f8 !important; +} + +.co-pre-line { + white-space: pre-line; +} + +.form-group.spaced { + margin-left: 1rem; + margin-top: 0.5rem; +} + +.form-group.spaced > *:not(:last-child) { + margin-right: 0.5rem; +} \ No newline at end of file diff --git a/web/src/components/forms/dynamic-form/dynamic-form.tsx b/web/src/components/forms/dynamic-form/dynamic-form.tsx new file mode 100644 index 000000000..4ae9b631b --- /dev/null +++ b/web/src/components/forms/dynamic-form/dynamic-form.tsx @@ -0,0 +1,112 @@ +import { ErrorBoundaryFallbackProps } from '@openshift-console/dynamic-plugin-sdk'; +import { Accordion, Alert } from '@patternfly/react-core'; +import Form, { FormProps } from '@rjsf/core'; +import * as _ from 'lodash'; +import * as React from 'react'; +import { useTranslation } from 'react-i18next'; +import { getUpdatedCR } from '../utils'; +import { K8S_UI_SCHEMA } from './const'; +import './dynamic-form.css'; +import { ErrorBoundary } from './error-boundary'; +import defaultFields from './fields'; +import defaultTemplates, { ErrorTemplate } from './templates'; +import defaultWidgets from './widgets'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type DynamicFormProps = FormProps & { + errors?: string[]; + ErrorTemplate?: React.FC<{ errors: string[] }>; + customUISchema?: boolean; + showAlert?: boolean; +}; + +export const DynamicFormFormErrorFallback: React.FC = () => { + const { t } = useTranslation('plugin__netobserv-plugin'); + + return ( + + ); +}; + +export const DynamicForm: React.FC = ({ + errors = [], + fields = {}, + templates = {}, + formContext, + formData = {}, + noValidate = false, + onFocus = _.noop, + onBlur = _.noop, + onChange = _.noop, + onError = _.noop, + schema, + uiSchema = {}, + widgets = {}, + customUISchema, + showAlert = false, + ...restProps +}) => { + const { t } = useTranslation('plugin__netobserv-plugin'); + const [mustSync, setMustSync] = React.useState(false); + return ( +
+ {showAlert && ( + + )} + + +
{ + // skip the onChange event if formData is not ready + if (!event.formData || !event.formData.apiVersion || !event.formData.kind) { + return; + } + if (!mustSync) { + // keep original formData reference and update only specific fields + event.formData = getUpdatedCR(formData, event.formData); + } + setMustSync(false); + onChange(event, id); + }} + onFocus={(id, data) => { + setMustSync(true); + onFocus(id, data); + }} + onBlur={onBlur} + onError={onError} + schema={schema} + // Don't show the react-jsonschema-form error list at top + showErrorList={false} + uiSchema={customUISchema ? uiSchema : _.defaultsDeep({}, K8S_UI_SCHEMA, uiSchema)} + widgets={{ ...defaultWidgets, ...widgets }} + templates={{ ...defaultTemplates, ...templates }} + > + <>{errors.length > 0 && } + +
+
+
+ ); +}; + +export default DynamicForm; diff --git a/web/src/components/forms/dynamic-form/error-boundary.tsx b/web/src/components/forms/dynamic-form/error-boundary.tsx new file mode 100644 index 000000000..09bda28e3 --- /dev/null +++ b/web/src/components/forms/dynamic-form/error-boundary.tsx @@ -0,0 +1,61 @@ +import { ErrorBoundaryFallbackProps } from '@openshift-console/dynamic-plugin-sdk'; +import * as React from 'react'; + +export type ErrorBoundaryProps = { + fallbackComponent?: React.ComponentType; +}; + +export type ErrorBoundaryState = { + hasError: boolean; + error: { message: string; stack: string; name: string }; + errorInfo: { componentStack: string }; +}; + +const DefaultFallback: React.FC = () =>
; + +export class ErrorBoundary extends React.Component { + readonly defaultState: ErrorBoundaryState = { + hasError: false, + error: { + message: '', + stack: '', + name: '' + }, + errorInfo: { + componentStack: '' + } + }; + + constructor(props: ErrorBoundaryProps | Readonly) { + super(props); + this.state = this.defaultState; + } + + componentDidCatch(error: never, errorInfo: never) { + this.setState({ + hasError: true, + error, + errorInfo + }); + // Log the error so something shows up in the JS console when `DefaultFallback` is used. + // eslint-disable-next-line no-console + console.error('Caught error in a child component:', error, errorInfo); + } + + render() { + const { hasError, error, errorInfo } = this.state; + const FallbackComponent = this.props.fallbackComponent || DefaultFallback; + return hasError ? ( + + ) : ( + <>{this.props.children} + ); + } +} + +export default ErrorBoundary; diff --git a/web/src/components/forms/dynamic-form/fields.tsx b/web/src/components/forms/dynamic-form/fields.tsx new file mode 100644 index 000000000..499467095 --- /dev/null +++ b/web/src/components/forms/dynamic-form/fields.tsx @@ -0,0 +1,148 @@ +import { + AccordionContent, + AccordionItem, + AccordionToggle, + Button, + Flex, + FlexItem, + Popover +} from '@patternfly/react-core'; +import { FieldProps, UiSchema } from '@rjsf/utils'; +import classnames from 'classnames'; +import { JSONSchema7 } from 'json-schema'; +import * as React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useTheme } from '../../../utils/theme-hook'; +import { useSchemaDescription, useSchemaLabel } from './utils'; + +export const Description: React.FC<{ + id?: string; + label?: string; + description?: string; + border?: boolean; + padding?: boolean; +}> = ({ id, label, description, border, padding }) => { + const isDarkTheme = useTheme(); + + if (!description) { + return null; + } + + const parts = description.split('\n'); + let content = <>{description}; + if (parts.length > 1) { + content = ( + {content}
} + > + + + ); + } + + return ( + +
+ {content} +
+
+ ); +}; + +export type DescriptionFieldProps = Pick & { + defaultLabel?: string; +}; + +export const DescriptionField: React.FC = ({ + id, + description, + defaultLabel, + schema, + uiSchema +}) => { + const [, label] = useSchemaLabel(schema, uiSchema || {}, defaultLabel); + return ; +}; + +export type FormFieldProps = { + id: string; + defaultLabel?: string; + required: boolean; + schema: JSONSchema7; + uiSchema: UiSchema; +}; + +export const FormField: React.FC = ({ children, id, defaultLabel, required, schema, uiSchema }) => { + const { t } = useTranslation('plugin__netobserv-plugin'); + const [showLabel, label] = useSchemaLabel(schema, uiSchema, defaultLabel || t('Value')); + + return ( +
+ {showLabel && label ? ( + + + + + {children} + + ) : ( + children + )} +
+ ); +}; + +export type FieldSetProps = Pick & { + defaultLabel?: string; +}; + +export const FieldSet: React.FC = props => { + const { children, defaultLabel, idSchema, required = false, schema, uiSchema } = props; + const [expanded, setExpanded] = React.useState(idSchema['$id'] === 'root'); // root is expanded by default + const [showLabel, label] = useSchemaLabel(schema, uiSchema || {}, defaultLabel); + const description = useSchemaDescription(schema, uiSchema || {}); + return showLabel && label ? ( +
+ + setExpanded(!expanded)} + > + + + {description && ( + + )} + + {children} + + +
+ ) : ( + <>{children} + ); +}; + +// no default fields as these are imported from templates +export default {}; diff --git a/web/src/components/forms/dynamic-form/templates.tsx b/web/src/components/forms/dynamic-form/templates.tsx new file mode 100644 index 000000000..2a014025f --- /dev/null +++ b/web/src/components/forms/dynamic-form/templates.tsx @@ -0,0 +1,166 @@ +import { Alert, Button, Divider, FormHelperText } from '@patternfly/react-core'; +import { MinusCircleIcon } from '@patternfly/react-icons/dist/esm/icons/minus-circle-icon'; +import { PlusCircleIcon } from '@patternfly/react-icons/dist/esm/icons/plus-circle-icon'; +import { + ArrayFieldTemplateProps, + DescriptionFieldProps, + FieldTemplateProps, + getSchemaType, + getUiOptions, + ObjectFieldTemplateProps +} from '@rjsf/utils'; +import * as _ from 'lodash'; +import * as React from 'react'; +import { useTranslation } from 'react-i18next'; +import { JSON_SCHEMA_GROUP_TYPES } from './const'; +import { DescriptionField, FieldSet, FormField } from './fields'; +import { UiSchemaOptionsWithDependency } from './types'; +import { useSchemaLabel } from './utils'; + +export const AtomicFieldTemplate: React.FC = ({ + children, + id, + label, + rawErrors, + description, + required, + schema, + uiSchema +}) => { + // put description before or after children based on widget type + const descriptionFirst = uiSchema?.['ui:descriptionFirst'] === 'true'; + return ( + + {descriptionFirst && description} + {children} + {!descriptionFirst && description} + {!_.isEmpty(rawErrors) && ( + <> + {_.map(rawErrors, error => ( + {_.capitalize(error)} + ))} + + )} + + ); +}; + +export const DescriptionFieldTemplate: React.FC = props => { + return ; +}; + +export const FieldTemplate: React.FC = props => { + const { id, hidden, schema = {}, children, uiSchema = {}, formContext = {} } = props; + const type = getSchemaType(schema); + const [dependencyMet, setDependencyMet] = React.useState(true); + React.useEffect(() => { + const { dependency } = getUiOptions(uiSchema ?? {}) as UiSchemaOptionsWithDependency; // Type defs for this function are awful + if (dependency) { + setDependencyMet(() => { + let val = _.get(formContext.formData ?? {}, ['spec'], ''); + dependency.controlFieldPath.forEach(path => { + val = _.get(val, [path], ''); + if (Array.isArray(val)) { + // retreive id from path + // example root_spec_exporters_4_ipfix will return 4 + val = val[Number(id.replace(/\D/g, ''))]; + } + }); + + return dependency?.controlFieldValue === String(val); + }); + } + }, [uiSchema, formContext, id]); + + if (hidden || !dependencyMet) { + return null; + } + const isGroup = JSON_SCHEMA_GROUP_TYPES.includes(String(type)); + return isGroup ? children : ; +}; + +export const ObjectFieldTemplate: React.FC = props => { + const { idSchema, properties, required, schema, title, uiSchema } = props; + const { flat } = getUiOptions(uiSchema ?? {}); + if (flat === 'true') { + return <>{_.map(properties || [], p => p.content)}; + } + + return ( +
+
+ {properties?.length > 0 && _.map(properties, p => p.content)} +
+
+ ); +}; + +export const ArrayFieldTemplate: React.FC = ({ + idSchema, + items, + onAddClick, + required, + schema, + title, + uiSchema +}) => { + const { t } = useTranslation('plugin__netobserv-plugin'); + const [, label] = useSchemaLabel(schema, uiSchema || {}, title ?? 'Items'); + return ( +
+ {_.map(items ?? [], item => { + return ( +
+ {item.index > 0 && } + {item.hasRemove && ( +
+ +
+ )} + {item.children} +
+ ); + })} +
+ +
+
+ ); +}; + +export const ErrorTemplate: React.FC<{ errors: string[] }> = ({ errors }) => { + const { t } = useTranslation('plugin__netobserv-plugin'); + return ( + + {t('Fix the following errors:')} +
    + {_.map(errors, error => ( +
  • {error}
  • + ))} +
+
+ ); +}; + +export default { + FieldTemplate, + DescriptionFieldTemplate, + ArrayFieldTemplate, + ObjectFieldTemplate +}; diff --git a/web/src/components/forms/dynamic-form/types.ts b/web/src/components/forms/dynamic-form/types.ts new file mode 100644 index 000000000..73e0939d4 --- /dev/null +++ b/web/src/components/forms/dynamic-form/types.ts @@ -0,0 +1,14 @@ +export type DynamicFormFieldDependency = { + controlFieldPath: string[]; + controlFieldValue: string; + controlFieldName: string; +}; + +export type UiSchemaOptionsWithDependency = { + dependency?: DynamicFormFieldDependency; +}; + +export type DynamicFormSchemaError = { + title: string; + message: string; +}; diff --git a/web/src/components/forms/dynamic-form/utils.spec.ts b/web/src/components/forms/dynamic-form/utils.spec.ts new file mode 100644 index 000000000..509b547c1 --- /dev/null +++ b/web/src/components/forms/dynamic-form/utils.spec.ts @@ -0,0 +1,57 @@ +import { prune } from './utils'; + +const PRUNE_DATA = { + abc: { + '123': {} + }, + test1: { + num: NaN, + str: '', + bool: null + }, + test2: { + num: NaN, + str: '', + bool: null + }, + test3: { + child: { + grandchild: {} + } + }, + test4: { + arr1: [NaN, '', undefined, null, {}], + arr2: [] + } +}; + +const PRUNE_SAMPLE = { + test2: {}, + test3: { + child: {} + }, + test4: { + arr1: [] + } +}; + +describe('prune', () => { + it('Prunes all empty data when no sample is provided', () => { + const result = prune(PRUNE_DATA); + expect(result.abc).toBeUndefined(); + expect(result.test1).toBeUndefined(); + expect(result.test2).toBeUndefined(); + expect(result.test3).toBeUndefined(); + expect(result.test4).toBeUndefined(); + }); + + it('Only prunes empty data without explicit empty samples', () => { + const result = prune(PRUNE_DATA, PRUNE_SAMPLE); + expect(result.abc).toBeUndefined(); + expect(result.test1).toBeUndefined(); + expect(result.test2).toEqual({}); + expect(result.test3.child).toEqual({}); + expect(result.test4.arr1).toEqual([]); + expect(result.test4.arr2).toBeUndefined(); + }); +}); diff --git a/web/src/components/forms/dynamic-form/utils.ts b/web/src/components/forms/dynamic-form/utils.ts new file mode 100644 index 000000000..52e27b68f --- /dev/null +++ b/web/src/components/forms/dynamic-form/utils.ts @@ -0,0 +1,72 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { getUiOptions, UiSchema } from '@rjsf/utils'; +import { JSONSchema7 } from 'json-schema'; +import * as _ from 'lodash'; +import { DynamicFormSchemaError } from './types'; + +const UNSUPPORTED_SCHEMA_PROPERTIES = ['allOf', 'anyOf', 'oneOf']; + +export const useSchemaLabel = (schema: JSONSchema7, uiSchema: UiSchema, defaultLabel?: string) => { + const options = getUiOptions(uiSchema ?? {}); + const showLabel = options?.label ?? true; + const label = (options?.title || schema?.title) as string; + return [showLabel, label || defaultLabel] as [boolean, string]; +}; + +export const useSchemaDescription = (schema: JSONSchema7, uiSchema: UiSchema, defaultDescription?: string) => + (getUiOptions(uiSchema ?? {})?.description || schema?.description || defaultDescription) as string; + +export const getSchemaErrors = (schema: JSONSchema7): DynamicFormSchemaError[] => { + return [ + ...(_.isEmpty(schema) + ? [ + { + title: 'Empty Schema', + message: 'Schema is empty.' + } + ] + : []), + ..._.map(_.intersection(_.keys(schema), UNSUPPORTED_SCHEMA_PROPERTIES), unsupportedProperty => ({ + title: 'Unsupported Property', + message: `Cannot generate form fields for JSON schema with ${unsupportedProperty} property.` + })) + ]; +}; + +// Returns true if a value is not nil and is empty +export const definedAndEmpty = (value: any): boolean => !_.isNil(value) && _.isEmpty(value); + +// Helper function for prune +export const pruneRecursive = (current: any, sample: any): any => { + const valueIsEmpty = (value: any, key: string | number) => + _.isNil(value) || + _.isNaN(value) || + (_.isString(value) && _.isEmpty(value)) || + (_.isObject(value) && _.isEmpty(pruneRecursive(value, sample?.[key]))); + + // Value should be pruned if it is empty and the correspondeing sample is not explicitly + // defined as an empty value. + const shouldPrune = (value: any, key: string) => valueIsEmpty(value, key) && !definedAndEmpty(sample?.[key]); + + // Prune each property of current value that meets the pruning criteria + _.forOwn(current, (value, key) => { + if (shouldPrune(value, key)) { + delete current[key]; + } + }); + + // remove any leftover undefined values from the delete operation on an array + if (_.isArray(current)) { + _.pull(current, undefined); + } + + return current; +}; + +// Deeply remove all empty, NaN, null, or undefined values from an object or array. If a value meets +// the above criteria, but the corresponding sample is explicitly defined as an empty vaolue, it +// will not be pruned. +// Based on https://stackoverflow.com/a/26202058/8895304 +export const prune = (obj: any, sample?: any): any => { + return pruneRecursive(_.cloneDeep(obj), sample); +}; diff --git a/web/src/components/forms/dynamic-form/widgets.tsx b/web/src/components/forms/dynamic-form/widgets.tsx new file mode 100644 index 000000000..d0e11d9fa --- /dev/null +++ b/web/src/components/forms/dynamic-form/widgets.tsx @@ -0,0 +1,224 @@ +import { CodeEditor, Language } from '@patternfly/react-code-editor'; +import { + Checkbox, + Dropdown, + DropdownItem, + Flex, + FlexItem, + MenuToggle, + MenuToggleElement, + Switch +} from '@patternfly/react-core'; +import { getSchemaType, WidgetProps } from '@rjsf/utils'; +import classNames from 'classnames'; +import * as _ from 'lodash'; +import * as React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useTheme } from '../../../utils/theme-hook'; +import { JSON_SCHEMA_NUMBER_TYPES } from './const'; +import { DescriptionField } from './fields'; +import { AtomicFieldTemplate } from './templates'; + +export const TextWidget: React.FC = props => { + const { disabled = false, id, onBlur, onChange, onFocus, readonly = false, schema = {}, value = '' } = props; + const schemaType = getSchemaType(schema); + return JSON_SCHEMA_NUMBER_TYPES.includes(String(schemaType)) ? ( + + ) : ( + + onBlur(id, event.target.value))} + onChange={({ currentTarget }) => onChange(currentTarget.value, undefined, id)} + onFocus={onFocus && (event => onFocus(id, event.target.value))} + readOnly={readonly} + type="text" + value={value} + /> + + ); +}; + +export const NumberWidget: React.FC = props => { + const { value, id, onBlur, onChange, onFocus } = props; + const numberValue = _.toNumber(value); + return ( + + onBlur(id, event.target.value))} + onChange={({ currentTarget }) => + onChange(currentTarget.value !== '' ? _.toNumber(currentTarget.value) : '', undefined, id) + } + onFocus={onFocus && (event => onFocus(id, event.target.value))} + type="number" + value={_.isFinite(numberValue) ? numberValue : ''} + /> + + ); +}; + +export const PasswordWidget: React.FC = props => { + const { value = '', id, onBlur, onChange, onFocus } = props; + return ( + + onBlur(id, event.target.value))} + onChange={({ currentTarget }) => onChange(currentTarget.value, undefined, id)} + onFocus={onFocus && (event => onFocus(id, event.target.value))} + value={value} + /> + + ); +}; + +export const SwitchWidget: React.FC = props => { + const { t } = useTranslation('plugin__netobserv-plugin'); + const { value, id, label, onBlur, onChange, onFocus } = props; + return ( + onBlur(id, event.target.value))} + onChange={(_event, v) => onChange(v, undefined, id)} + onFocus={onFocus && (event => onFocus(id, event.target.value))} + label={t('Enabled')} + labelOff={t('Disabled')} + /> + ); +}; + +export const SelectWidget: React.FC = props => { + const { t } = useTranslation('plugin__netobserv-plugin'); + const { id, label, onBlur, onChange, onFocus, options, schema, value } = props; + const [isOpen, setIsOpen] = React.useState(false); + const { enumOptions = [], title } = options; + return ( + onBlur(id, value))} + onSelect={(e, v) => { + onChange(v, undefined, id); + setIsOpen(false); + }} + onFocus={onFocus && (event => onFocus(id, value))} + onOpenChange={(isOpen: boolean) => setIsOpen(isOpen)} + toggle={(toggleRef: React.Ref) => ( + setIsOpen(!isOpen)} isExpanded={isOpen}> + {value || t('Select {{title}}', { title: title || schema?.title || label })} + + )} + > + {enumOptions.map(option => ( + + {option.label} + + ))} + + ); +}; + +export const JSONWidget: React.FC = props => { + const isDarkTheme = useTheme(); + + const { disabled = false, id, onBlur, onChange, onFocus, readonly = false, value = '{}' } = props; + return ( + + onBlur(id, value))} + onChange={v => onChange(v, undefined, id)} + onFocus={onFocus && (() => onFocus(id, value))} + language={Language.json} + height="75px" + /> + + ); +}; + +export const ArrayCheckboxesWidget: React.FC = props => { + const { schema, value, id, onBlur, onChange, onFocus } = props; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const enums = (schema.items as any).enum || []; + const errFunc = () => console.error('Function not implemented.'); + + return ( + // since schema type is 'array' and widget is 'checkboxes', we use AtomicFieldTemplate + // to render the field and values all at once without the add/remove buttons + errFunc} + onDropPropertyClick={() => errFunc} + description={ + + } + {...{ + ...props, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + style: props.style as React.StyleHTMLAttributes | undefined, + readonly: props.readonly === true, + disabled: props.disabled === true + }} + > + onBlur(id, value)} + onFocus={() => onFocus(id, value)} + > + {enums.map((option: string, index: number) => ( + + + onChange( + value.includes(option) ? value.filter((v: string) => v !== option) : [...value, option], + undefined, + id + ) + } + /> + + ))} + + + ); +}; + +export default { + BaseInput: TextWidget, + CheckboxWidget: SwitchWidget, // force using switch everywhere for consistency + SwitchWidget, + NumberWidget, + PasswordWidget, + SelectWidget, + TextWidget, + int32: NumberWidget, + int64: NumberWidget, + map: JSONWidget, + arrayCheckboxes: ArrayCheckboxesWidget +}; diff --git a/web/src/components/forms/editor-toggle.tsx b/web/src/components/forms/editor-toggle.tsx new file mode 100644 index 000000000..b398da388 --- /dev/null +++ b/web/src/components/forms/editor-toggle.tsx @@ -0,0 +1,101 @@ +import { ActionGroup, Alert, Button, Flex, FlexItem, Radio } from '@patternfly/react-core'; +import React, { FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ContextSingleton } from '../../utils/context'; +import './forms.css'; + +export enum EditorType { + CUSTOM = 'custom', + YAML = 'yaml' +} + +type EditorToggleProps = { + type: EditorType; + onChange: (newValue: EditorType) => void; + onSubmit: () => void; + onCancel: () => void; + customChild: JSX.Element; + yamlChild: JSX.Element; + updated: boolean; + isUpdate: boolean; + onReload: () => void; +}; + +export const EditorToggle: FC = ({ + type, + onChange, + onSubmit, + onCancel, + onReload, + customChild, + yamlChild, + updated, + isUpdate +}) => { + const { t } = useTranslation('plugin__netobserv-plugin'); + + return ( + + + + + checked && onChange(EditorType.CUSTOM)} + value={EditorType.CUSTOM} + /> + checked && onChange(EditorType.YAML)} + value={EditorType.YAML} + /> + + + + {type === EditorType.CUSTOM ? customChild : yamlChild} + + {(type === EditorType.CUSTOM || ContextSingleton.isStandalone()) && ( + + {updated && ( + + {t('Click reload to see the new version.')} + + )} + + + {updated && ( + + )} + + + + )} + + ); +}; diff --git a/web/src/components/forms/flowCollector-status.tsx b/web/src/components/forms/flowCollector-status.tsx new file mode 100644 index 000000000..1fbc095a6 --- /dev/null +++ b/web/src/components/forms/flowCollector-status.tsx @@ -0,0 +1,56 @@ +import React, { FC } from 'react'; + +import { Flex, FlexItem, PageSection, Title } from '@patternfly/react-core'; +import { useTranslation } from 'react-i18next'; +import DynamicLoader from '../dynamic-loader/dynamic-loader'; +import { GetFlowCollectorJS } from './config/templates'; +import './forms.css'; +import { Pipeline } from './pipeline'; +import { ResourceStatus } from './resource-status'; +import { Consumer, ResourceWatcher } from './resource-watcher'; + +export type FlowCollectorStatusProps = {}; + +export const FlowCollectorStatus: FC = props => { + const { t } = useTranslation('plugin__netobserv-plugin'); + const [selectedTypes, setSelectedTypes] = React.useState([]); + + return ( + + + + {({ group, version, kind, existing }) => { + return ( + + + + {existing && ( + + + + )} + + + + + + ); + }} + + + + ); +}; + +export default FlowCollectorStatus; diff --git a/web/src/components/forms/flowCollector-wizard.tsx b/web/src/components/forms/flowCollector-wizard.tsx new file mode 100644 index 000000000..1a26d7355 --- /dev/null +++ b/web/src/components/forms/flowCollector-wizard.tsx @@ -0,0 +1,186 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { ResourceYAMLEditor } from '@openshift-console/dynamic-plugin-sdk'; +import { PageSection, Title, Wizard, WizardStep, WizardStepChangeScope, WizardStepType } from '@patternfly/react-core'; +import validator from '@rjsf/validator-ajv8'; +import React, { FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ContextSingleton } from '../../utils/context'; +import { flowCollectorStatusPath } from '../../utils/url'; +import { safeYAMLToJS } from '../../utils/yaml'; +import DynamicLoader, { navigate } from '../dynamic-loader/dynamic-loader'; +import { FlowCollectorSchema } from './config/schema'; +import { GetFlowCollectorJS } from './config/templates'; +import { FlowCollectorUISchema } from './config/uiSchema'; +import Consumption from './consumption'; +import { DynamicForm } from './dynamic-form/dynamic-form'; +import './forms.css'; +import ResourceWatcher, { Consumer } from './resource-watcher'; +import { getFilteredUISchema } from './utils'; + +export type FlowCollectorWizardProps = {}; + +const defaultPaths = ['spec.namespace', 'spec.networkPolicy']; + +export const FlowCollectorWizard: FC = props => { + const { t } = useTranslation('plugin__netobserv-plugin'); + const [data, setData] = React.useState(null); + const [paths, setPaths] = React.useState(defaultPaths); + + const form = React.useCallback(() => { + const filteredSchema = getFilteredUISchema(FlowCollectorUISchema, paths); + return ( + { + setData(event.formData); + }} + /> + ); + }, [data, paths]); + + const step = React.useCallback( + (id, name: string) => { + return ( + + {form()} + + ); + }, + [form] + ); + + const onStepChange = React.useCallback( + ( + event: React.MouseEvent, + step: WizardStepType, + prevStep: WizardStepType, + scope: WizardStepChangeScope + ) => { + switch (step.id) { + case 'overview': + setPaths(defaultPaths); + break; + case 'capture': + setPaths([ + 'spec.agent.ebpf.sampling', + 'spec.agent.ebpf.privileged', + 'spec.agent.ebpf.features', + 'spec.processor.clusterName', + 'spec.processor.multiClusterDeployment', + 'spec.processor.addZone' + ]); + break; + case 'pipeline': + setPaths([ + 'spec.deploymentModel', + 'spec.kafka', + 'spec.processor.advanced.secondaryNetworks.items', + 'spec.exporters.items' + ]); + break; + case 'loki': + setPaths(['spec.loki']); + break; + case 'prom': + setPaths(['spec.prometheus.querier']); + break; + case 'console': + setPaths(['spec.consolePlugin.enable', 'spec.consolePlugin.replicas']); + break; + default: + setPaths([]); + } + }, + [] + ); + + const setSampling = React.useCallback( + (sampling: number) => { + if (!data) { + return; + } + data.spec.agent.ebpf.sampling = sampling; + setData({ ...data }); + }, + [data] + ); + + return ( + + { + navigate(flowCollectorStatusPath); + }} + > + + {ctx => { + // first init data when watch resource query got results + if (data == null) { + setData(ctx.existing || ctx.defaultData); + } + return ( + + + ctx.onSubmit(data)}> + + + {t( + // eslint-disable-next-line max-len + 'Network Observability Operator deploys a monitoring pipeline that consists in:\n - an eBPF agent, that generates network flows from captured packets\n - flowlogs-pipeline, a component that collects, enriches and exports these flows\n - a Console plugin for flows visualization with powerful filtering options, a topology representation and more\n\nFlow data is then available in multiple ways, each optional:\n - As Prometheus metrics\n - As raw flow logs stored in Grafana Loki\n - As raw flow logs exported to a collector\n\nThe FlowCollector resource is used to configure the operator and its managed components.\nThis setup will guide you on the common aspects of the FlowCollector configuration.' + )} +

+ {t('Operator configuration')} +
+ {form()} +
+ + {form()} + + + {form()} + + + + {form()} + + + + + } + > + { + const updatedData = safeYAMLToJS(content); + setData(updatedData); + ctx.onSubmit(updatedData); + }} + /> + +
+
+ ); + }} +
+
+
+ ); +}; + +export default FlowCollectorWizard; diff --git a/web/src/components/forms/flowCollector.tsx b/web/src/components/forms/flowCollector.tsx new file mode 100644 index 000000000..f4002df99 --- /dev/null +++ b/web/src/components/forms/flowCollector.tsx @@ -0,0 +1,22 @@ +import React, { FC } from 'react'; + +import DynamicLoader from '../dynamic-loader/dynamic-loader'; +import { FlowCollectorSchema } from './config/schema'; +import { GetFlowCollectorJS } from './config/templates'; +import { FlowCollectorUISchema } from './config/uiSchema'; +import { ResourceForm } from './resource-form'; +import { ResourceWatcher } from './resource-watcher'; + +export type FlowCollectorFormProps = {}; + +export const FlowCollectorForm: FC = props => { + return ( + + + + + + ); +}; + +export default FlowCollectorForm; diff --git a/web/src/components/forms/flowMetric-wizard.tsx b/web/src/components/forms/flowMetric-wizard.tsx new file mode 100644 index 000000000..38b364284 --- /dev/null +++ b/web/src/components/forms/flowMetric-wizard.tsx @@ -0,0 +1,131 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { ResourceYAMLEditor } from '@openshift-console/dynamic-plugin-sdk'; +import { PageSection, Title, Wizard, WizardStep, WizardStepChangeScope, WizardStepType } from '@patternfly/react-core'; +import validator from '@rjsf/validator-ajv8'; +import React, { FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ContextSingleton } from '../../utils/context'; +import { safeYAMLToJS } from '../../utils/yaml'; +import DynamicLoader from '../dynamic-loader/dynamic-loader'; +import { FlowMetricSchema } from './config/schema'; +import { GetFlowMetricJS } from './config/templates'; +import { FlowMetricUISchema } from './config/uiSchema'; +import { DynamicForm } from './dynamic-form/dynamic-form'; +import './forms.css'; +import ResourceWatcher, { Consumer } from './resource-watcher'; +import { getFilteredUISchema } from './utils'; + +export type FlowMetricWizardProps = {}; + +const defaultPaths = ['metadata.namespace', 'metadata.name']; + +export const FlowMetricWizard: FC = props => { + const { t } = useTranslation('plugin__netobserv-plugin'); + const [data, setData] = React.useState(null); + const [paths, setPaths] = React.useState(defaultPaths); + + const form = React.useCallback(() => { + const filteredSchema = getFilteredUISchema(FlowMetricUISchema, paths); + return ( + { + setData(event.formData); + }} + /> + ); + }, [data, paths]); + + const onStepChange = React.useCallback( + ( + event: React.MouseEvent, + step: WizardStepType, + prevStep: WizardStepType, + scope: WizardStepChangeScope + ) => { + switch (step.id) { + case 'overview': + setPaths(defaultPaths); + break; + case 'metric': + setPaths(['spec.metricName', 'spec.type', 'spec.buckets', 'spec.valueField', 'spec.divider', 'spec.labels']); + break; + case 'data': + setPaths(['spec.flatten', 'spec.remap', 'spec.direction', 'spec.filters']); + break; + case 'charts': + setPaths(['spec.charts']); + break; + default: + setPaths([]); + } + }, + [] + ); + + return ( + + + + {ctx => { + // first init data when watch resource query got results + if (data == null) { + setData(ctx.existing || ctx.defaultData); + } + return ( + + + ctx.onSubmit(data)}> + + + {t( + // eslint-disable-next-line max-len + 'You can create custom metrics out of the flowlogs data using the FlowMetric API. In every flowlogs data that is collected, there are a number of fields labeled per log, such as source name and destination name. These fields can be leveraged as Prometheus labels to enable the customization of cluster information on your dashboard.\nThis setup will guide you on the common aspects of the FlowMetric configuration.' + )} +

+ {t('General configuration')} +
+ {form()} +
+ + {form()} + + + {form()} + + + {form()} + + } + > + { + const updatedData = safeYAMLToJS(content); + setData(updatedData); + ctx.onSubmit(updatedData); + }} + /> + +
+
+ ); + }} +
+
+
+ ); +}; + +export default FlowMetricWizard; diff --git a/web/src/components/forms/flowMetric.tsx b/web/src/components/forms/flowMetric.tsx new file mode 100644 index 000000000..f15d2e2cc --- /dev/null +++ b/web/src/components/forms/flowMetric.tsx @@ -0,0 +1,22 @@ +import React, { FC } from 'react'; + +import DynamicLoader from '../dynamic-loader/dynamic-loader'; +import { FlowMetricSchema } from './config/schema'; +import { GetFlowMetricJS } from './config/templates'; +import { FlowMetricUISchema } from './config/uiSchema'; +import { ResourceForm } from './resource-form'; +import { ResourceWatcher } from './resource-watcher'; + +export type FlowMetricFormProps = {}; + +export const FlowMetricForm: FC = props => { + return ( + + + + + + ); +}; + +export default FlowMetricForm; diff --git a/web/src/components/forms/forms.css b/web/src/components/forms/forms.css new file mode 100644 index 000000000..025220587 --- /dev/null +++ b/web/src/components/forms/forms.css @@ -0,0 +1,64 @@ +#pageSection { + display: flex; + flex-direction: column; + height: 100%; + padding: 0; +} + +/* fix page section color for PF4 compat in OCP 4.15+ */ +#pageSection.light { + background: #fff; +} + +#pageSection.dark { + background: #1b1d21; +} + +#pageHeader { + padding: 1.5rem; +} + +#pageHeader>div { + align-items: start; +} + +.wizard-editor-container, +.wizard-editor-container>div { + display: flex; + flex-direction: column; + flex: 1 +} + +.editor-toggle-container { + height: 100%; +} + +.editor-toggle { + border-bottom: 1px solid #d2d2d2; + border-top: 1px solid #d2d2d2; + padding: 0.5rem 1.5rem; +} + +#editor-content-container { + display: flex; + flex-direction: column; +} + +#editor-toggle-footer { + margin-left: 1.5rem; + margin-right: 1.5rem; +} + +.status-container { + flex: 1; + margin-left: 1.5rem; + margin-right: 1.5rem; +} + +.status-list-container { + overflow-y: auto; +} + +.calculator-item { + padding-bottom: 1.5rem; +} \ No newline at end of file diff --git a/web/src/components/forms/pipeline.tsx b/web/src/components/forms/pipeline.tsx new file mode 100644 index 000000000..2ee6245ed --- /dev/null +++ b/web/src/components/forms/pipeline.tsx @@ -0,0 +1,290 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + K8sResourceCondition, + K8sResourceConditionStatus, + K8sResourceKind +} from '@openshift-console/dynamic-plugin-sdk'; + +import { + DefaultTaskGroup, + DEFAULT_EDGE_TYPE, + DEFAULT_FINALLY_NODE_TYPE, + DEFAULT_SPACER_NODE_TYPE, + DEFAULT_TASK_NODE_TYPE, + DEFAULT_WHEN_OFFSET, + FinallyNode, + getEdgesFromNodes, + getSpacerNodes, + Graph, + GraphComponent, + GRAPH_LAYOUT_END_EVENT, + Layout, + ModelKind, + Node, + PipelineDagreLayout, + PipelineNodeModel, + RunStatus, + SpacerNode, + TaskEdge, + TaskNode, + Visualization, + VisualizationProvider, + VisualizationSurface, + WhenDecorator +} from '@patternfly/react-topology'; + +import { getResizeObserver } from '@patternfly/react-core'; +import { t } from 'i18next'; +import _ from 'lodash'; +import * as React from 'react'; + +export interface Step { + id: string; + type?: string; + label: string; + runAfterTasks?: string[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data?: any; +} +export interface StepProps { + element: Node; +} + +export const StepNode: React.FunctionComponent = ({ element }) => { + const data = element.getData(); + + const whenDecorator = data?.whenStatus ? ( + + ) : null; + + return ( + data?.onSelect?.()}> + {whenDecorator} + + ); +}; + +const pipelineComponentFactory = (kind: ModelKind, type: string) => { + if (kind === ModelKind.graph) { + return GraphComponent; + } + switch (type) { + case DEFAULT_TASK_NODE_TYPE: + return StepNode; + case DEFAULT_FINALLY_NODE_TYPE: + return FinallyNode; + case 'task-group': + return DefaultTaskGroup; + case 'finally-group': + return DefaultTaskGroup; + case DEFAULT_SPACER_NODE_TYPE: + return SpacerNode; + case 'finally-spacer-edge': + case DEFAULT_EDGE_TYPE: + return TaskEdge; + default: + return undefined; + } +}; + +export type FlowCollectorPipelineProps = { + existing: K8sResourceKind | null; + selectedTypes: string[]; + setSelectedTypes: (types: string[]) => void; +}; + +export const Pipeline: React.FC = ({ existing, selectedTypes, setSelectedTypes }) => { + const containerRef = React.createRef(); + const [controller, setController] = React.useState(); + + const fit = React.useCallback(() => { + if (controller && controller.hasGraph()) { + controller.getGraph().fit(); + } else { + console.error('onResize called before controller graph'); + } + }, [controller]); + + const getStatus = React.useCallback( + (types: string[], status: string) => { + for (let i = 0; i < types.length; i++) { + const type = types[i]; + const condition: K8sResourceCondition | null = existing?.status?.conditions?.find( + (condition: K8sResourceCondition) => condition.type === type + ); + if (condition?.status !== status) { + if (condition?.status === 'Unknown') { + return RunStatus.Skipped; + } else if (condition?.type.startsWith('Waiting') || condition?.reason === 'Pending') { + return RunStatus.Pending; + } + return RunStatus.Failed; + } + } + return RunStatus.Succeeded; + }, + [existing?.status?.conditions] + ); + + const getSteps = React.useCallback(() => { + const steps: Step[] = []; + + if (existing?.spec?.agent?.type === 'eBPF') { + const types = ['Ready']; + steps.push({ + id: 'ebpf', + label: 'eBPF agents', + data: { + status: getStatus(types, K8sResourceConditionStatus.True), + selected: _.some(selectedTypes, t => types.includes(t)), + onSelect: () => setSelectedTypes(types) + } + }); + } + + const flpStatuses = ['WaitingFLPParent', 'WaitingFLPMonolith']; + if (existing?.spec?.deploymentModel === 'Kafka') { + const types = ['WaitingFLPTransformer']; + steps.push({ + id: 'kafka', + label: 'Kafka', + runAfterTasks: ['ebpf'], + data: { + status: getStatus(types, K8sResourceConditionStatus.False), + selected: _.some(selectedTypes, t => types.includes(t)), + onSelect: () => setSelectedTypes(types) + } + }); + flpStatuses.push(...types); + } + + if (existing?.spec) { + steps.push({ + id: 'flp', + label: 'Flowlogs pipeline', + runAfterTasks: [_.last(steps)!.id], + data: { + status: getStatus(flpStatuses, K8sResourceConditionStatus.False), + selected: _.some(selectedTypes, t => flpStatuses.includes(t)), + onSelect: () => setSelectedTypes(flpStatuses) + } + }); + } + + const cpRunAfter: string[] = []; + if (existing?.spec?.loki?.enable) { + const types = ['LokiIssue']; + steps.push({ + id: 'loki', + label: 'Loki', + runAfterTasks: ['flp'], + data: { + status: getStatus(types, 'NoIssue'), // TODO: NoIssue / Unknown is not a valid status. That should be False. + selected: _.some(selectedTypes, t => types.includes(t)), + onSelect: () => setSelectedTypes(types) + } + }); + cpRunAfter.push('loki'); + } + + if (existing?.spec?.prometheus?.querier?.enable) { + steps.push({ + id: 'prom', + label: 'Prometheus', + runAfterTasks: ['flp'], + data: { + onSelect: () => setSelectedTypes([]) + } + }); + cpRunAfter.push('prom'); + } + + if (existing?.spec?.exporters?.length) { + existing.spec.exporters.forEach((exporter: any, i: number) => { + steps.push({ + id: `exporter-${i}`, + label: exporter.type || t('Unknown'), + runAfterTasks: ['flp'], + data: { + onSelect: () => setSelectedTypes([]) + } + }); + }); + } + + if (existing?.spec?.consolePlugin?.enable && cpRunAfter.length) { + steps.push({ + id: 'plugin', + label: 'Console plugin', + runAfterTasks: cpRunAfter, + data: { + onSelect: () => setSelectedTypes([]) + } + }); + } + + return steps.map(s => ({ + type: s.type || 'DEFAULT_TASK_NODE', + width: 180, + height: 32, + style: { + padding: [45, 15] + }, + ...s + })) as PipelineNodeModel[]; + }, [existing, getStatus, selectedTypes, setSelectedTypes]); + + React.useEffect(() => { + if (containerRef.current) { + getResizeObserver( + containerRef.current, + () => { + fit(); + }, + true + ); + } + }, [containerRef, controller, fit]); + + React.useEffect(() => { + if (!controller) { + return; + } + const steps = getSteps(); + const spacerNodes = getSpacerNodes(steps); + const nodes = [...steps, ...spacerNodes]; + const edges = getEdgesFromNodes(steps); + controller.fromModel( + { + nodes, + edges, + graph: { + id: 'g1', + type: 'graph', + layout: 'pipelineLayout' + } + }, + false + ); + }, [controller, getSteps]); + + //create controller on startup and register factories + React.useEffect(() => { + const c = new Visualization(); + c.registerComponentFactory(pipelineComponentFactory); + c.registerLayoutFactory((type: string, graph: Graph): Layout | undefined => new PipelineDagreLayout(graph)); + c.addEventListener(GRAPH_LAYOUT_END_EVENT, fit); + setController(c); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( +
+ + + +
+ ); +}; + +export default Pipeline; diff --git a/web/src/components/forms/resource-form.tsx b/web/src/components/forms/resource-form.tsx new file mode 100644 index 000000000..d933cdf32 --- /dev/null +++ b/web/src/components/forms/resource-form.tsx @@ -0,0 +1,99 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { ResourceYAMLEditor } from '@openshift-console/dynamic-plugin-sdk'; +import { FormHelperText, PageSection, Title } from '@patternfly/react-core'; +import { UiSchema } from '@rjsf/utils'; +import _ from 'lodash'; +import React, { FC, Suspense } from 'react'; +import { useTranslation } from 'react-i18next'; +import { netflowTrafficPath } from '../../utils/url'; +import { safeYAMLToJS } from '../../utils/yaml'; +import { navigate } from '../dynamic-loader/dynamic-loader'; +import { SchemaValidator } from './config/validator'; +import { DynamicForm } from './dynamic-form/dynamic-form'; +import { EditorToggle, EditorType } from './editor-toggle'; +import './forms.css'; +import { Consumer } from './resource-watcher'; + +export type ResourceFormProps = { + schema: any; + uiSchema: UiSchema; +}; + +export const ResourceForm: FC = ({ schema, uiSchema }) => { + const { t } = useTranslation('plugin__netobserv-plugin'); + const [viewType, setViewType] = React.useState(EditorType.CUSTOM); + const [data, setData] = React.useState(null); + + const hasChanged = React.useCallback( + (existing: any) => { + return data?.metadata?.resourceVersion !== existing?.metadata?.resourceVersion; + }, + [data?.metadata?.resourceVersion] + ); + + return ( + + {({ kind, isUpdate, existing, defaultData, onSubmit, errors, setErrors }) => { + // first init data when watch resource query got results + if (data == null) { + setData(existing || defaultData); + } + return ( + + + }> + setData(existing)} + onChange={type => { + setViewType(type); + }} + onSubmit={() => { + onSubmit(data); + }} + onCancel={() => navigate(netflowTrafficPath)} + customChild={ + setErrors(_.map(errs, error => error.stack))} + onChange={(event, id) => { + setData(event.formData); + }} + /> + } + yamlChild={ + { + const updatedData = safeYAMLToJS(content); + setData(updatedData); + onSubmit(updatedData); + }} + /> + } + /> + + + ); + }} + + ); +}; + +export default ResourceForm; diff --git a/web/src/components/forms/resource-status.tsx b/web/src/components/forms/resource-status.tsx new file mode 100644 index 000000000..8118412fb --- /dev/null +++ b/web/src/components/forms/resource-status.tsx @@ -0,0 +1,73 @@ +import { K8sResourceCondition, K8sResourceKind } from '@openshift-console/dynamic-plugin-sdk'; +import { Button, Text, TextVariants } from '@patternfly/react-core'; +import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; +import React, { FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import { navigate } from '../dynamic-loader/dynamic-loader'; + +export type ResourceStatusProps = { + group: string; + version: string; + kind: string; + existing: K8sResourceKind | null; + selectedTypes: string[]; + setSelectedTypes: (types: string[]) => void; +}; + +export const ResourceStatus: FC = ({ + group, + version, + kind, + existing, + selectedTypes, + setSelectedTypes +}) => { + const { t } = useTranslation('plugin__netobserv-plugin'); + + if (!existing) { + return ( + <> + {t("{{kind}} resource doesn't exists yet.", { kind })} + + + ); + } + + const conditions = (existing?.status?.conditions || []) as K8sResourceCondition[]; + return ( + + + + + + + + + + + + {conditions.map((condition, i) => ( + setSelectedTypes([condition.type])} + > + + + + + + + ))} + +
{t('Type')}{t('Status')}{t('Reason')}{t('Message')}{t('Changed')}
{condition.type}{condition.status}{condition.reason}{condition.message}{condition.lastTransitionTime}
+ ); +}; + +export default ResourceStatus; diff --git a/web/src/components/forms/resource-watcher.tsx b/web/src/components/forms/resource-watcher.tsx new file mode 100644 index 000000000..6c3e9ccd3 --- /dev/null +++ b/web/src/components/forms/resource-watcher.tsx @@ -0,0 +1,109 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { k8sCreate, K8sResourceKind, k8sUpdate, useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; +import { Bullseye, Spinner } from '@patternfly/react-core'; +import React, { FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useK8sModel } from '../../utils/k8s-models-hook'; +import { ErrorComponent } from '../messages/error'; +import { prune } from './dynamic-form/utils'; +import './forms.css'; + +export type ResourceWatcherProps = { + defaultData: K8sResourceKind; + onSuccess?: (data: any) => void; + children: JSX.Element; +}; + +export type ResourceWatcherContext = { + group: string; + version: string; + kind: string; + isUpdate: boolean; + existing: K8sResourceKind | null; + defaultData: K8sResourceKind; + onSubmit: (data: K8sResourceKind) => void; + errors: string[]; + setErrors: (errors: string[]) => void; +}; + +export const { Provider, Consumer } = React.createContext({ + group: '', + version: '', + kind: '', + isUpdate: false, + existing: null, + defaultData: {}, + onSubmit: () => { + console.error('onSubmit is not initialized !'); + }, + errors: [], + setErrors: (errs: string[]) => { + console.error('setErrors is not initialized !', errs); + } +}); + +export const ResourceWatcher: FC = ({ defaultData, onSuccess, children }) => { + if (!defaultData.apiVersion) { + throw new Error('ResourceForm error: apiVersion must be provided'); + } else if (!defaultData.kind) { + throw new Error('ResourceForm error: kind must be provided'); + } else if (!defaultData.metadata || !defaultData.metadata.name) { + throw new Error('ResourceForm error: name must be provided'); + } + const { t } = useTranslation('plugin__netobserv-plugin'); + const apiVersion = defaultData.apiVersion; + const groupVersion = apiVersion.split('/'); + const group = groupVersion[0]; + const version = groupVersion[1]; + const kind = defaultData.kind; + const model = useK8sModel(group, version, kind); + const [resources, loaded, loadError] = useK8sWatchResource({ + groupVersionKind: { + group: groupVersion[0], + version: groupVersion[1], + kind + }, + name: defaultData.metadata.name, + isList: true // use list to avoid object issue + }); + const [errors, setErrors] = React.useState([]); + + if (loadError) { + return ; + } else if (!loaded) { + return ( + + + + ); + } + return ( + 0, + existing: resources?.length ? { apiVersion, kind, ...resources[0] } : null, + defaultData, + errors, + setErrors, + onSubmit: data => { + const apiFunc = resources?.length > 0 ? k8sUpdate : k8sCreate; + apiFunc({ + data: prune(data), + model + }) + .then(res => { + onSuccess && onSuccess(res); + }) + .catch(e => setErrors([e.message])); + } + }} + > + {children} + + ); +}; + +export default ResourceWatcher; diff --git a/web/src/components/forms/utils.ts b/web/src/components/forms/utils.ts new file mode 100644 index 000000000..f4ef8b881 --- /dev/null +++ b/web/src/components/forms/utils.ts @@ -0,0 +1,74 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { UiSchema } from '@rjsf/utils'; +import _ from 'lodash'; + +export const appendRecursive = (obj: any, key: string, value?: string) => { + if (!obj) { + return obj; + } + + const originalKey = `${key}_original`; + if (value !== undefined) { + // backup original value if exists + if (obj[key]) { + obj[originalKey] = obj[key]; + } + // set key / value + obj[key] = value; + } else if (obj[originalKey]) { + // restore original key + obj[key] = obj[originalKey]; + } else { + // delete the key + delete obj[key]; + } + + // recursively apply key and value on all children objects + Object.keys(obj).forEach(k => { + if (typeof obj[k] === 'object') { + obj[k] = appendRecursive(obj[k], key, value); + } + }); + return obj; +}; + +export const setFlat = (obj: any) => { + if (!obj) { + return obj; + } + + // show current object + delete obj['ui:widget']; + // hide accordion + obj['ui:flat'] = 'true'; + return obj; +}; + +export const getFilteredUISchema = (ui: UiSchema, paths: string[]) => { + // clone provided ui schema to avoid altering original object + const clonedSchema = _.cloneDeep(ui); + // hide all the fields + const filteredUi = appendRecursive(clonedSchema, 'ui:widget', 'hidden'); + // show expected ones + paths.forEach((path: string) => { + const keys = path.split('.'); + let current = filteredUi; + keys.forEach(key => { + setFlat(current); + // move to next item + current = current[key]; + }); + setFlat(current); + // show all the fields under specified path + current = appendRecursive(current, 'ui:widget'); + }); + + return filteredUi; +}; + +export const getUpdatedCR = (data: any, updatedData: any) => { + // only update metadata and spec + data.metadata = updatedData.metadata; + data.spec = updatedData.spec; + return data; +}; diff --git a/web/src/components/messages/error.tsx b/web/src/components/messages/error.tsx index faa19b863..ed0d0f740 100644 --- a/web/src/components/messages/error.tsx +++ b/web/src/components/messages/error.tsx @@ -43,7 +43,7 @@ export interface ErrorProps { isLokiRelated: boolean; } -export const Error: React.FC = ({ title, error, isLokiRelated }) => { +export const ErrorComponent: React.FC = ({ title, error, isLokiRelated }) => { const { t } = useTranslation('plugin__netobserv-plugin'); const [lokiLoading, setLokiLoading] = React.useState(isLokiRelated); const [statusLoading, setStatusLoading] = React.useState(true); @@ -357,4 +357,4 @@ export const Error: React.FC = ({ title, error, isLokiRelated }) => ); }; -export default Error; +export default ErrorComponent; diff --git a/web/src/components/toolbar/filters-toolbar.css b/web/src/components/toolbar/filters-toolbar.css index 8245d22a5..7d2766869 100644 --- a/web/src/components/toolbar/filters-toolbar.css +++ b/web/src/components/toolbar/filters-toolbar.css @@ -45,11 +45,6 @@ button.pf-v5-c-button.pf-v5-m-link.pf-v5-m-inline:empty { padding: 5px 0px 0px 5px; } -#filters { - width: 100%; - margin-top: 1em; -} - /* align toolbar with title and table */ div#filter-toolbar-search-filters { padding: 0; diff --git a/web/src/utils/k8s-models-hook.ts b/web/src/utils/k8s-models-hook.ts index 367a227d9..7cef780fe 100644 --- a/web/src/utils/k8s-models-hook.ts +++ b/web/src/utils/k8s-models-hook.ts @@ -77,3 +77,8 @@ export function useK8sModelsWithColors() { return k8sModels; } + +export function useK8sModel(group: string, version: string, kind: string) { + const [k8sModels] = useK8sModels(); + return k8sModels[`${group}~${version}~${kind}`]; +} diff --git a/web/src/utils/url.ts b/web/src/utils/url.ts index e049f0c33..2afdcafd6 100644 --- a/web/src/utils/url.ts +++ b/web/src/utils/url.ts @@ -2,6 +2,7 @@ import _ from 'lodash'; import { navigate } from '../components/dynamic-loader/dynamic-loader'; export const netflowTrafficPath = '/netflow-traffic'; +export const flowCollectorStatusPath = '/k8s/cluster/flows.netobserv.io~v1beta2~FlowCollector/status'; // React-router query argument (not backend routes) export enum URLParam { diff --git a/web/src/utils/yaml.ts b/web/src/utils/yaml.ts new file mode 100644 index 000000000..a2f314606 --- /dev/null +++ b/web/src/utils/yaml.ts @@ -0,0 +1,22 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { dump, load } from 'js-yaml'; + +// Safely parse js obj to yaml. Returns fallback (emtpy string by default) on exception. +export const safeJSToYAML = (js: any, fallback = '', options: any = {}): string => { + try { + return dump(js, options); + } catch (err) { + console.error(err); + return fallback; + } +}; + +// Safely parse yaml to js object. Returns fallback (empty object by default) on exception. +export const safeYAMLToJS = (yaml: string, fallback: any = {}, options: any = {}): any => { + try { + return load(yaml, options); + } catch (err) { + console.error(err); + return fallback; + } +}; diff --git a/web/webpack.config.ts b/web/webpack.config.ts index 9451a80d8..ffa1e2c62 100644 --- a/web/webpack.config.ts +++ b/web/webpack.config.ts @@ -86,7 +86,301 @@ module.exports = { ], }, plugins: [ - new ConsoleRemotePlugin(), + new ConsoleRemotePlugin({ + pluginMetadata: { + name: "netobserv-plugin", + version: "0.1.0", + displayName: "NetObserv Plugin for OCP Console", + description: "This plugin adds network observability pages to Openshift console", + exposedModules: { + "netflowParent": "./components/netflow-traffic-parent.tsx", + "netflowTab": "./components/netflow-traffic-tab.tsx", + "netflowDevTab": "./components/netflow-traffic-dev-tab.tsx", + }, + }, + extensions: [ + { + type: "console.navigation/href", + properties: { + "id": "netflow-traffic-link", + "perspective": "admin", + "section": "observe", + name: "%plugin__netobserv-plugin~Network Traffic%", + "href": "/netflow-traffic" + } + }, + { + type: "console.navigation/href", + properties: { + "id": "netflow-traffic-link-projectadmin", + "perspective": "admin", + "section": "observe-projectadmin", + name: "%plugin__netobserv-plugin~Network Traffic%", + "href": "/netflow-traffic" + }, + "flags": { "disallowed": ["CAN_LIST_NS"] } + }, + { + type: "console.page/route", + properties: { + path: "/netflow-traffic", + component: { + "$codeRef": "netflowParent.default" + } + } + }, + { + type: "console.tab/horizontalNav", + properties: { + model: { + version: "v1", + group: "", + kind: "Pod" + }, + component: { + "$codeRef": "netflowTab.default" + }, + "page": { + name: "%plugin__netobserv-plugin~Network Traffic%", + "href": "netflow" + } + } + }, + { + type: "console.tab/horizontalNav", + properties: { + model: { + version: "v1", + group: "", + kind: "Service" + }, + component: { + "$codeRef": "netflowTab.default" + }, + "page": { + name: "%plugin__netobserv-plugin~Network Traffic%", + "href": "netflow" + } + } + }, + { + type: "console.tab/horizontalNav", + properties: { + model: { + version: "v1", + group: "", + kind: "Namespace" + }, + component: { + "$codeRef": "netflowTab.default" + }, + "page": { + name: "%plugin__netobserv-plugin~Network Traffic%", + "href": "netflow" + } + } + }, + { + type: "console.tab/horizontalNav", + properties: { + model: { + version: "v1", + group: "apps", + kind: "Deployment" + }, + component: { + "$codeRef": "netflowTab.default" + }, + "page": { + name: "%plugin__netobserv-plugin~Network Traffic%", + "href": "netflow" + } + } + }, + { + type: "console.tab/horizontalNav", + properties: { + model: { + version: "v1", + group: "apps", + kind: "StatefulSet" + }, + component: { + "$codeRef": "netflowTab.default" + }, + "page": { + name: "%plugin__netobserv-plugin~Network Traffic%", + "href": "netflow" + } + } + }, + { + type: "console.tab/horizontalNav", + properties: { + model: { + version: "v1", + group: "apps", + kind: "DaemonSet" + }, + component: { + "$codeRef": "netflowTab.default" + }, + "page": { + name: "%plugin__netobserv-plugin~Network Traffic%", + "href": "netflow" + } + } + }, + { + type: "console.tab/horizontalNav", + properties: { + model: { + version: "v1", + group: "apps", + kind: "ReplicaSet" + }, + component: { + "$codeRef": "netflowTab.default" + }, + "page": { + name: "%plugin__netobserv-plugin~Network Traffic%", + "href": "netflow" + } + } + }, + { + type: "console.tab/horizontalNav", + properties: { + model: { + version: "v1", + group: "", + kind: "Node" + }, + component: { + "$codeRef": "netflowTab.default" + }, + "page": { + name: "%plugin__netobserv-plugin~Network Traffic%", + "href": "netflow" + } + } + }, + { + type: "console.tab/horizontalNav", + properties: { + model: { + version: "v1", + group: "batch", + kind: "CronJob" + }, + component: { + "$codeRef": "netflowTab.default" + }, + "page": { + name: "%plugin__netobserv-plugin~Network Traffic%", + "href": "netflow" + } + } + }, + { + type: "console.tab/horizontalNav", + properties: { + model: { + version: "v1", + group: "batch", + kind: "Job" + }, + component: { + "$codeRef": "netflowTab.default" + }, + "page": { + name: "%plugin__netobserv-plugin~Network Traffic%", + "href": "netflow" + } + } + }, + { + type: "console.tab/horizontalNav", + properties: { + model: { + version: "v2beta2", + group: "autoscaling", + kind: "HorizontalPodAutoscaler" + }, + component: { + "$codeRef": "netflowTab.default" + }, + "page": { + name: "%plugin__netobserv-plugin~Network Traffic%", + "href": "netflow" + } + } + }, + { + type: "console.tab/horizontalNav", + properties: { + model: { + version: "v1", + group: "route.openshift.io", + kind: "Route" + }, + component: { + "$codeRef": "netflowTab.default" + }, + "page": { + name: "%plugin__netobserv-plugin~Network Traffic%", + "href": "netflow" + } + } + }, + { + type: "console.tab/horizontalNav", + properties: { + model: { + version: "v1", + group: "k8s.ovn.org", + kind: "ClusterUserDefinedNetwork" + }, + component: { + "$codeRef": "netflowTab.default" + }, + "page": { + name: "%plugin__netobserv-plugin~Network Traffic%", + "href": "netflow" + } + } + }, + { + type: "console.tab/horizontalNav", + properties: { + model: { + version: "v1", + group: "k8s.ovn.org", + kind: "UserDefinedNetwork" + }, + component: { + "$codeRef": "netflowTab.default" + }, + "page": { + name: "%plugin__netobserv-plugin~Network Traffic%", + "href": "netflow" + } + } + }, + { + type: "console.tab", + properties: { + "contextId": "dev-console-observe", + name: "%plugin__netobserv-plugin~Network Traffic%", + "href": "netflow-traffic", + component: { + "$codeRef": "netflowDevTab.default" + } + } + } + ], + validateExtensionIntegrity: false // must be skipped to avoid modules not referenced error + }), new CopyWebpackPlugin({ patterns: [ { from: path.resolve(__dirname, 'locales'), to: 'locales' }, @@ -101,6 +395,120 @@ module.exports = { }, }; +if (process.env.FLAVOR === 'static') { + module.exports.output.path = path.resolve(__dirname, 'dist', 'static'); + module.exports.plugins = [ + new ConsoleRemotePlugin({ + pluginMetadata: { + name: "netobserv-plugin-static", + version: "0.1.0", + displayName: "NetObserv Static Plugin for OCP Console", + description: "This plugin adds custom forms for FlowCollector and FlowMetrics API", + exposedModules: { + "yamlTemplates": "./components/forms/config/templates.ts", + "flowCollectorWizard": "./components/forms/flowCollector-wizard.tsx", + "flowCollectorForm": "./components/forms/flowCollector.tsx", + "flowCollectorStatus": "./components/forms/flowCollector-status.tsx", + "flowMetricWizard": "./components/forms/flowMetric-wizard.tsx", + "flowMetricForm": "./components/forms/flowMetric.tsx" + }, + }, + extensions: [ + { + type: "console.yaml-template", + properties: { + model: { + version: "v1beta2", + group: "flows.netobserv.io", + kind: "FlowCollector" + }, + name: "default", + template: { + "$codeRef": "yamlTemplates.FlowCollector" + } + } + }, + { + type: "console.page/route", + properties: { + // add FlowCollector wizard to 'Installed Operator' -> 'Create' action + path: "/k8s/ns/:namespace/operators.coreos.com~v1alpha1~ClusterServiceVersion/:operator/flows.netobserv.io~v1beta2~FlowCollector/~new", + component: { + "$codeRef": "flowCollectorWizard.default" + } + } + }, + { + type: "console.page/route", + properties: { + path: [ + // add FlowCollector form to standard 'New' and 'Edit' actions + "/k8s/cluster/flows.netobserv.io~v1beta2~FlowCollector/~new", + "/k8s/cluster/flows.netobserv.io~v1beta2~FlowCollector/:name" + ], + component: { + "$codeRef": "flowCollectorForm.default" + } + } + }, + { + type: "console.page/route", + properties: { + path: "/k8s/cluster/flows.netobserv.io~v1beta2~FlowCollector/status", + component: { + "$codeRef": "flowCollectorStatus.default" + } + } + }, + { + type: "console.yaml-template", + properties: { + model: { + version: "v1alpha1", + group: "flows.netobserv.io", + kind: "FlowMetric" + }, + name: "default", + template: { + "$codeRef": "yamlTemplates.FlowMetric" + } + } + }, + { + type: "console.page/route", + properties: { + // add FlowMetric wizard to 'Installed Operator' -> 'Create' action + path: "k8s/ns/:namespace/operators.coreos.com~v1alpha1~ClusterServiceVersion/:operator/flows.netobserv.io~v1alpha1~FlowMetric/~new", + component: { + "$codeRef": "flowMetricWizard.default" + } + } + }, + { + type: "console.page/route", + properties: { + path: [ + // add FlowMetric form to 'Installed Operator' -> 'Edit' action and standard 'New' and 'Edit' actions + "/k8s/ns/:namespace/clusterserviceversions/:operator/flows.netobserv.io~v1alpha1~FlowMetric/:name", + "/k8s/cluster/flows.netobserv.io~v1alpha1~FlowMetric/~new", + "/k8s/cluster/flows.netobserv.io~v1alpha1~FlowMetric/:name" + ], + component: { + "$codeRef": "flowMetricForm.default" + } + } + } + ], + }), + new CopyWebpackPlugin({ + patterns: [ + { from: path.resolve(__dirname, 'locales'), to: 'locales' }, + { from: path.resolve(__dirname, 'assets'), to: 'assets' }, + ], + }), + ]; +} + if (process.env.NODE_ENV === 'production') { module.exports.mode = 'production'; module.exports.output.filename = '[name]-bundle-[hash].min.js';