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 415258951..f0a08ce0d 100644 --- a/Dockerfile.cypress +++ b/Dockerfile.cypress @@ -9,6 +9,8 @@ WORKDIR /opt/app-root COPY --chown=node web/package.json web/package.json COPY --chown=node web/package-lock.json web/package-lock.json +COPY --chown=node web/manage-extension.sh web/manage-extension.sh + WORKDIR /opt/app-root/web RUN npm ci @@ -19,6 +21,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 39e5ebc2d..d5e55b81a 100644 --- a/Dockerfile.downstream +++ b/Dockerfile.downstream @@ -7,6 +7,8 @@ WORKDIR /opt/app-root COPY --chown=default web/package.json web/package.json COPY --chown=default web/package-lock.json web/package-lock.json +COPY --chown=node web/manage-extension.sh web/manage-extension.sh + WORKDIR /opt/app-root/web RUN CYPRESS_INSTALL_BINARY=0 node --max-old-space-size=6000 $(which npm) --legacy-peer-deps ci --ignore-scripts @@ -18,6 +20,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..c4f7c0673 100644 --- a/Dockerfile.front +++ b/Dockerfile.front @@ -7,6 +7,8 @@ WORKDIR /opt/app-root COPY --chown=node web/package-lock.json web/package-lock.json COPY --chown=node web/package.json web/package.json +COPY --chown=node web/manage-extension.sh web/manage-extension.sh + WORKDIR /opt/app-root/web RUN CYPRESS_INSTALL_BINARY=0 npm --legacy-peer-deps ci @@ -17,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 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 4d1540f51..d047455af 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -145,12 +145,16 @@ 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, @@ -242,7 +246,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 09e6b7c16..b5f10df97 100644 --- a/pkg/server/routes.go +++ b/pkg/server/routes.go @@ -39,26 +39,32 @@ func setupRoutes(ctx context.Context, cfg *config.Config, authChecker auth.Check // Server status 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)) + + // 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/locales/en/plugin__netobserv-plugin.json b/web/locales/en/plugin__netobserv-plugin.json index 2e87bf23a..4eca63db4 100644 --- a/web/locales/en/plugin__netobserv-plugin.json +++ b/web/locales/en/plugin__netobserv-plugin.json @@ -159,6 +159,81 @@ "S": "S", "XS": "XS", "None": "None", + "Cluster metrics": "Cluster metrics", + "Bandwidth": "Bandwidth", + "Nodes": "Nodes", + "Namespaces": "Namespaces", + "Pods": "Pods", + "Recommendations": "Recommendations", + "The example outlined in the table demonstrates a scenario that is tailored to your workload. Consider this example only as a baseline from which adjustments can be made to accommodate your needs.": "The example outlined in the table demonstrates a scenario that is tailored to your workload. Consider this example only as a baseline from which adjustments can be made to accommodate your needs.", + "vCPU": "vCPU", + "Memory": "Memory", + "LokiStack size": "LokiStack size", + "Kafka": "Kafka", + "Performance tuning and estimation": "Performance tuning and estimation", + "The sampling interval is one of the main settings used to balance performance and accuracy. The lower the interval, the higher the accuracy.": "The sampling interval is one of the main settings used to balance performance and accuracy. The lower the interval, the higher the accuracy.", + "Use the slider below to configure the desired sampling interval.": "Use the slider below to configure the desired sampling interval.", + "Sampling interval": "Sampling interval", + "The estimations are based on the number of nodes in the cluster and the sampling rate. They do not take into account the number of namespaces or pods, as their impact is comparatively lower than that of nodes.\nThey are calculated using a linear regression model based on data collected from various OpenShift clusters. Actual resource consumption may vary depending on your specific workload and cluster configuration.": "The estimations are based on the number of nodes in the cluster and the sampling rate. They do not take into account the number of namespaces or pods, as their impact is comparatively lower than that of nodes.\nThey are calculated using a linear regression model based on data collected from various OpenShift clusters. Actual resource consumption may vary depending on your specific workload and cluster configuration.", + "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.", + "(see more...)": "(see more...)", + "Remove {{singularLabel}}": "Remove {{singularLabel}}", + "Add {{singularLabel}}": "Add {{singularLabel}}", + "Error": "Error", + "Fix the following errors:": "Fix the following errors:", + "True": "True", + "False": "False", + "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", + "Delete": "Delete", + "Network Observability FlowCollector status": "Network Observability FlowCollector status", + "Edit FlowCollector": "Edit FlowCollector", + "Open Network Traffic page": "Open Network Traffic page", + "An error occured while retreiving FlowCollector: {{error}}": "An error occured while retreiving FlowCollector: {{error}}", + "Create FlowCollector": "Create FlowCollector", + "Network Observability FlowCollector setup": "Network Observability FlowCollector setup", + "Overview": "Overview", + "The FlowCollector resource is used to configure the Network Observability operator and its managed components. When it is created, network flows start being collected.": "The FlowCollector resource is used to configure the Network Observability operator and its managed components. When it is created, network flows start being collected.", + "This wizard is a helper to create a first FlowCollector resource. It does not cover all the available configuration options, but only the most common ones.\nFor advanced configuration, please use YAML or the": "This wizard is a helper to create a first FlowCollector resource. It does not cover all the available configuration options, but only the most common ones.\nFor advanced configuration, please use YAML or the", + "FlowCollector form": "FlowCollector form", + ", which includes more options such as:\n- Filtering options\n- Configuring custom exporters\n- Custom labels based on IP\n- Pod identification for secondary networks\n- Performance fine-tuning\nYou can always edit a FlowCollector later when you start with the simplified configuration.": ", which includes more options such as:\n- Filtering options\n- Configuring custom exporters\n- Custom labels based on IP\n- Pod identification for secondary networks\n- Performance fine-tuning\nYou can always edit a FlowCollector later when you start with the simplified configuration.", + "Operator configuration": "Operator configuration", + "Processing": "Processing", + "Consumption": "Consumption", + "Submit": "Submit", + "Network Observability FlowMetric setup": "Network Observability FlowMetric setup", + "You can create custom metrics out of the network flows using the FlowMetric API. A FlowCollector resource must be created as well in order to produce the flows. Each flow consists in a set of fields with values, such as source name and destination name. These fields can be leveraged as Prometheus labels to enable customized metrics and dashboards.": "You can create custom metrics out of the network flows using the FlowMetric API. A FlowCollector resource must be created as well in order to produce the flows. Each flow consists in a set of fields with values, such as source name and destination name. These fields can be leveraged as Prometheus labels to enable customized metrics and dashboards.", + "This simplified setup guides you through the common aspects of the FlowMetric configuration. For advanced configuration, please use YAML or the ": "This simplified setup guides you through the common aspects of the FlowMetric configuration. For advanced configuration, please use YAML or the ", + "FlowMetric form": "FlowMetric form", + "Resource configuration": "Resource configuration", + "Metric": "Metric", + "Data": "Data", + "Review": "Review", + "Unknown": "Unknown", + "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.", + "Delete {{kind}}?": "Delete {{kind}}?", + "This action cannot be undone.": "This action cannot be undone.", + "It will destroy all pods, services and other objects in the namespace": "It will destroy all pods, services and other objects in the namespace", + "The following metric will not be collected anymore": "The following metric will not be collected anymore", + "{{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 {{index}}/{{count}}": "Step {{index}}/{{count}}", "Step {{index}}/{{count}}_plural": "Step {{index}}/{{count}}", "Previous tip": "Previous tip", @@ -231,7 +306,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", @@ -350,7 +424,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", @@ -397,7 +470,6 @@ "Export view": "Export view", "Observe": "Observe", "External": "External", - "Unknown": "Unknown", "Last 5 minutes": "Last 5 minutes", "Last 15 minutes": "Last 15 minutes", "Last 30 minutes": "Last 30 minutes", diff --git a/web/console-extensions.json b/web/manage-extension.sh old mode 100644 new mode 100755 similarity index 68% rename from web/console-extensions.json rename to web/manage-extension.sh index eff2d360a..fb358f184 --- a/web/console-extensions.json +++ b/web/manage-extension.sh @@ -1,4 +1,78 @@ -[ +#!/bin/bash + +if [ "$1" == "static" ]; then + echo '[ + { + "type": "console.page/route", + "properties": { + "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": [ + "/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.page/route", + "properties": { + "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": [ + "/k8s/ns/:namespace/clusterserviceversions/:operator/flows.netobserv.io~v1alpha1~FlowMetric/:name", + "/k8s/ns/:namespace/flows.netobserv.io~v1alpha1~FlowMetric/~new", + "/k8s/ns/:namespace/flows.netobserv.io~v1alpha1~FlowMetric/:name" + ], + "component": { + "$codeRef": "flowMetricForm.default" + } + } + } +]' > console-extensions.json + echo $(cat package.json | jq '.consolePlugin = { + "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": { + "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" + }, + "dependencies": { + "@console/pluginAPI": "*" + } + }') > package.json +else + echo '[ { "type": "console.flag", "properties": { @@ -286,4 +360,19 @@ } } } -] \ No newline at end of file +]' > console-extensions.json + echo $(cat package.json | jq '.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": "*" + } + }') > package.json +fi \ No newline at end of file diff --git a/web/moduleMapper/dummy.tsx b/web/moduleMapper/dummy.tsx index 8d3b2c115..97ee71c31 100644 --- a/web/moduleMapper/dummy.tsx +++ b/web/moduleMapper/dummy.tsx @@ -1,7 +1,13 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { K8sGroupVersionKind, K8sModel, K8sResourceKindReference, 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 { ResourceIconProps, ResourceLinkProps } from '@openshift-console/dynamic-plugin-sdk'; import { useK8sModelsWithColors } from '../src/utils/k8s-models-hook'; +import { safeJSToYAML } from '../src/utils/yaml'; import { k8sModels } from './k8s-models'; +import { FlowCollectorSchema, FlowMetricSchema } from './schemas'; +import { GetFlowCollectorJS, GetFlowMetricJS } from './templates'; // This dummy file is used to resolve @Console imports from @openshift-console for JEST / Standalone // You can add any exports needed here @@ -35,6 +41,210 @@ 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 k8sDelete(k8s: any): Promise { + console.log("k8sDelete", 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(() => { + if (!req) { + console.error("useK8sWatchResource: No request provided"); + return; + } + + const kind = req.kind || req.groupVersionKind.kind; + // simulate a loading + if (resource == null) { + setTimeout(() => { + switch (kind) { + case 'CustomResourceDefinition': + if (req.name === 'flowcollectors.flows.netobserv.io') { + setResource({ + apiVersion: 'apiextensions.k8s.io/v1', + kind: 'CustomResourceDefinition', + metadata: { + name: req.name + }, + spec: { + group: 'flows.netobserv.io', + names: { + kind: 'FlowCollector', + plural: 'flowcollectors' + }, + scope: 'Cluster', + versions: [{ + name: 'v1beta2', + served: true, + storage: true, + schema: { + openAPIV3Schema: FlowCollectorSchema, + } + }] + } + }); + } else { + setResource({ + apiVersion: 'apiextensions.k8s.io/v1', + kind: 'CustomResourceDefinition', + metadata: { + name: req.name + }, + spec: { + group: 'flows.netobserv.io', + names: { + kind: 'FlowMetric', + plural: 'flowmetrics' + }, + scope: 'Namespaced', + versions: [{ + name: 'v1alpha1', + served: true, + storage: true, + schema: { + openAPIV3Schema: FlowMetricSchema + } + }] + } + }); + } + break; + 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; + case 'FlowMetric': + if (req.name === 'flowmetric-sample') { + const fm = _.cloneDeep(GetFlowMetricJS()); + fm.spec!.metricName = 'test_metric'; + setResource(fm); + } + break; + } + setLoaded(true); + }, 1000); + } + }, [req, resource]); + + return React.useMemo(() => { + if (!resource) { + return [null, loaded, null]; + } else { + return [resource, loaded, null]; + } + }, [loaded, resource]); +} + export const ResourceIcon: React.FC = ({ className, kind, @@ -80,4 +290,103 @@ export const ResourceLink: React.FC = ({ {children} ); -}; \ No newline at end of file +}; + + +export const ResourceYAMLEditor: React.FC = ({ + initialResource, + header, + onSave, +}) => { + 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", + queryRange = "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/moduleMapper/schemas.ts b/web/moduleMapper/schemas.ts new file mode 100644 index 000000000..f8d0f3e56 --- /dev/null +++ b/web/moduleMapper/schemas.ts @@ -0,0 +1,6130 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable max-len */ +import { RJSFSchema } from '@rjsf/utils'; + +export const FlowCollectorSchema: RJSFSchema | any = { + title: 'FlowCollector', + description: + 'The schema for the network flows collection API, which pilots and configures the underlying deployments.', + 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' + }, + name: { + type: 'string', + default: 'cluster' + }, + labels: { + type: 'object', + properties: {}, + additionalProperties: { + type: 'string' + } + } + }, + required: ['name'] + }, + spec: { + type: 'object', + description: + 'Defines the desired state of the FlowCollector resource.\n\n*: the mention of "unsupported" or "deprecated" for a feature throughout this document means that this feature\nis not officially supported by Red Hat. It might have been, for example, contributed by the community\nand accepted without a formal agreement for maintenance. The product maintainers might provide some support\nfor these features as a best effort only.', + properties: { + namespace: { + description: 'Namespace where network observability pods are deployed.', + type: 'string', + default: 'netobserv', + format: 'k8s-name' + }, + deploymentModel: { + description: + '`deploymentModel` defines the desired type of deployment for flow processing. Possible values are:\n- `Direct` (default) to make the flow processor listen directly from the agents.\n- `Kafka` to make flows sent to a Kafka pipeline before consumption by the processor.\nKafka can provide better scalability, resiliency, and high availability (for more details, see https://www.redhat.com/en/topics/integration/what-is-apache-kafka).', + type: 'string', + default: 'Direct', + enum: ['Direct', 'Kafka'] + }, + kafka: { + description: + 'Kafka configuration, allowing to use Kafka as a broker as part of the flow collection pipeline. Available when the `spec.deploymentModel` is `Kafka`.', + 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: '' + } + } + }, + agent: { + description: 'Agent configuration for flows extraction.', + type: 'object', + properties: { + ebpf: { + description: + '`ebpf` describes the settings related to the eBPF-based flow reporter when `spec.agent.type`\nis set to `eBPF`.', + type: 'object', + properties: { + sampling: { + description: + 'Sampling interval of the flow reporter. 100 means one flow on 100 is sent. 0 or 1 means all flows are sampled.', + type: 'integer', + format: 'int32', + default: 50, + minimum: 0 + }, + privileged: { + description: + 'Privileged mode for the eBPF Agent container. When ignored or set to `false`, the operator sets\ngranular capabilities (BPF, PERFMON, NET_ADMIN, SYS_RESOURCE) to the container.\nIf for some reason these capabilities cannot be set, such as if an old kernel version not knowing CAP_BPF\nis in use, then you can turn on this mode for more global privileges.\nSome agent features require the privileged mode, such as packet drops tracking (see `features`) and SR-IOV support.', + type: 'boolean' + }, + features: { + description: + 'List of additional features to enable. They are all disabled by default. Enabling additional features might have performance impacts. Possible values are:\n- `PacketDrop`: Enable the packets drop flows logging feature. This feature requires mounting\nthe kernel debug filesystem, so the eBPF agent pods must run as privileged.\nIf the `spec.agent.ebpf.privileged` parameter is not set, an error is reported.\n- `DNSTracking`: Enable the DNS tracking feature.\n- `FlowRTT`: Enable flow latency (sRTT) extraction in the eBPF agent from TCP traffic.\n- `NetworkEvents`: Enable the network events monitoring feature, such as correlating flows and network policies.\nThis feature requires mounting the kernel debug filesystem, so the eBPF agent pods must run as privileged.\nIt requires using the OVN-Kubernetes network plugin with the Observability feature.\nIMPORTANT: This feature is available as a Technology Preview.\n- `PacketTranslation`: Enable enriching flows with packet translation information, such as Service NAT.\n- `EbpfManager`: [Unsupported (*)]. Use eBPF Manager to manage network observability eBPF programs. Pre-requisite: the eBPF Manager operator (or upstream bpfman operator) must be installed.\n- `UDNMapping`: [Unsupported (*)]. Enable interfaces mapping to User Defined Networks (UDN). \nThis feature requires mounting the kernel debug filesystem, so the eBPF agent pods must run as privileged.\nIt requires using the OVN-Kubernetes network plugin with the Observability feature.', + type: 'array', + items: { + description: + 'Agent feature, can be one of:\n- `PacketDrop`, to track packet drops.\n- `DNSTracking`, to track specific information on DNS traffic.\n- `FlowRTT`, to track TCP latency.\n- `NetworkEvents`, to track network events [Technology Preview].\n- `PacketTranslation`, to enrich flows with packets translation information, such as Service NAT.\n- `EbpfManager`, to enable using eBPF Manager to manage network observability eBPF programs. [Unsupported (*)].\n- `UDNMapping`, to enable interfaces mapping to UDN. [Unsupported (*)].', + type: 'string', + enum: [ + 'PacketDrop', + 'DNSTracking', + 'FlowRTT', + 'NetworkEvents', + 'PacketTranslation', + 'EbpfManager', + 'UDNMapping' + ] + } + }, + flowFilter: { + description: '`flowFilter` defines the eBPF agent configuration regarding flow filtering.', + type: 'object', + properties: { + enable: { + description: 'Set `enable` to `true` to enable the eBPF flow filtering feature.', + type: 'boolean' + }, + tcpFlags: { + description: + '`tcpFlags` optionally defines TCP flags to filter flows by.\nIn addition to the standard flags (RFC-9293), you can also filter by one of the three following combinations: `SYN-ACK`, `FIN-ACK`, and `RST-ACK`.', + type: 'string', + enum: ['SYN', 'SYN-ACK', 'ACK', 'FIN', 'RST', 'URG', 'ECE', 'CWR', 'FIN-ACK', 'RST-ACK'] + }, + sampling: { + description: + '`sampling` sampling interval for the matched flows, overriding the global sampling defined at `spec.agent.ebpf.sampling`.', + type: 'integer', + format: 'int32' + }, + peerIP: { + description: + '`peerIP` optionally defines the remote IP address to filter flows by.\nExample: `10.10.10.10`.', + type: 'string' + }, + icmpCode: { + description: + '`icmpCode`, for Internet Control Message Protocol (ICMP) traffic, optionally defines the ICMP code to filter flows by.', + type: 'integer' + }, + pktDrops: { + description: '`pktDrops` optionally filters only flows containing packet drops.', + type: 'boolean' + }, + destPorts: { + description: + '`destPorts` optionally defines the destination ports to filter flows by.\nTo filter a single port, set a single port as an integer value. For example, `destPorts: 80`.\nTo filter a range of ports, use a "start-end" range in string format. For example, `destPorts: "80-100"`.\nTo filter two ports, use a "port1,port2" in string format. For example, `ports: "80,100"`.', + anyOf: [ + { + type: 'integer' + }, + { + type: 'string' + } + ], + 'x-kubernetes-int-or-string': true + }, + ports: { + description: + '`ports` optionally defines the ports to filter flows by. It is used both for source and destination ports.\nTo filter a single port, set a single port as an integer value. For example, `ports: 80`.\nTo filter a range of ports, use a "start-end" range in string format. For example, `ports: "80-100"`.\nTo filter two ports, use a "port1,port2" in string format. For example, `ports: "80,100"`.', + anyOf: [ + { + type: 'integer' + }, + { + type: 'string' + } + ], + 'x-kubernetes-int-or-string': true + }, + cidr: { + description: + '`cidr` defines the IP CIDR to filter flows by.\nExamples: `10.10.10.0/24` or `100:100:100:100::/64`', + type: 'string' + }, + action: { + description: + '`action` defines the action to perform on the flows that match the filter. The available options are `Accept`, which is the default, and `Reject`.', + type: 'string', + enum: ['Accept', 'Reject'] + }, + peerCIDR: { + description: + '`peerCIDR` defines the Peer IP CIDR to filter flows by.\nExamples: `10.10.10.0/24` or `100:100:100:100::/64`', + type: 'string' + }, + sourcePorts: { + description: + '`sourcePorts` optionally defines the source ports to filter flows by.\nTo filter a single port, set a single port as an integer value. For example, `sourcePorts: 80`.\nTo filter a range of ports, use a "start-end" range in string format. For example, `sourcePorts: "80-100"`.\nTo filter two ports, use a "port1,port2" in string format. For example, `ports: "80,100"`.', + anyOf: [ + { + type: 'integer' + }, + { + type: 'string' + } + ], + 'x-kubernetes-int-or-string': true + }, + rules: { + description: + '`rules` defines 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.\n[Unsupported (*)].', + type: 'array', + maxItems: 16, + minItems: 1, + items: { + description: + '`EBPFFlowFilterRule` defines the desired eBPF agent configuration regarding flow filtering rule.', + type: 'object', + properties: { + tcpFlags: { + description: + '`tcpFlags` optionally defines TCP flags to filter flows by.\nIn addition to the standard flags (RFC-9293), you can also filter by one of the three following combinations: `SYN-ACK`, `FIN-ACK`, and `RST-ACK`.', + type: 'string', + enum: ['SYN', 'SYN-ACK', 'ACK', 'FIN', 'RST', 'URG', 'ECE', 'CWR', 'FIN-ACK', 'RST-ACK'] + }, + sampling: { + description: + '`sampling` sampling interval for the matched flows, overriding the global sampling defined at `spec.agent.ebpf.sampling`.', + type: 'integer', + format: 'int32' + }, + peerIP: { + description: + '`peerIP` optionally defines the remote IP address to filter flows by.\nExample: `10.10.10.10`.', + type: 'string' + }, + icmpCode: { + description: + '`icmpCode`, for Internet Control Message Protocol (ICMP) traffic, optionally defines the ICMP code to filter flows by.', + type: 'integer' + }, + pktDrops: { + description: '`pktDrops` optionally filters only flows containing packet drops.', + type: 'boolean' + }, + destPorts: { + description: + '`destPorts` optionally defines the destination ports to filter flows by.\nTo filter a single port, set a single port as an integer value. For example, `destPorts: 80`.\nTo filter a range of ports, use a "start-end" range in string format. For example, `destPorts: "80-100"`.\nTo filter two ports, use a "port1,port2" in string format. For example, `ports: "80,100"`.', + anyOf: [ + { + type: 'integer' + }, + { + type: 'string' + } + ], + 'x-kubernetes-int-or-string': true + }, + ports: { + description: + '`ports` optionally defines the ports to filter flows by. It is used both for source and destination ports.\nTo filter a single port, set a single port as an integer value. For example, `ports: 80`.\nTo filter a range of ports, use a "start-end" range in string format. For example, `ports: "80-100"`.\nTo filter two ports, use a "port1,port2" in string format. For example, `ports: "80,100"`.', + anyOf: [ + { + type: 'integer' + }, + { + type: 'string' + } + ], + 'x-kubernetes-int-or-string': true + }, + cidr: { + description: + '`cidr` defines the IP CIDR to filter flows by.\nExamples: `10.10.10.0/24` or `100:100:100:100::/64`', + type: 'string' + }, + action: { + description: + '`action` defines the action to perform on the flows that match the filter. The available options are `Accept`, which is the default, and `Reject`.', + type: 'string', + enum: ['Accept', 'Reject'] + }, + peerCIDR: { + description: + '`peerCIDR` defines the Peer IP CIDR to filter flows by.\nExamples: `10.10.10.0/24` or `100:100:100:100::/64`', + type: 'string' + }, + sourcePorts: { + description: + '`sourcePorts` optionally defines the source ports to filter flows by.\nTo filter a single port, set a single port as an integer value. For example, `sourcePorts: 80`.\nTo filter a range of ports, use a "start-end" range in string format. For example, `sourcePorts: "80-100"`.\nTo filter two ports, use a "port1,port2" in string format. For example, `ports: "80,100"`.', + anyOf: [ + { + type: 'integer' + }, + { + type: 'string' + } + ], + 'x-kubernetes-int-or-string': true + }, + icmpType: { + description: + '`icmpType`, for ICMP traffic, optionally defines the ICMP type to filter flows by.', + type: 'integer' + }, + protocol: { + description: + '`protocol` optionally defines a protocol to filter flows by. The available options are `TCP`, `UDP`, `ICMP`, `ICMPv6`, and `SCTP`.', + type: 'string', + enum: ['TCP', 'UDP', 'ICMP', 'ICMPv6', 'SCTP'] + }, + direction: { + description: + '`direction` optionally defines a direction to filter flows by. The available options are `Ingress` and `Egress`.', + type: 'string', + enum: ['Ingress', 'Egress'] + } + } + } + }, + icmpType: { + description: '`icmpType`, for ICMP traffic, optionally defines the ICMP type to filter flows by.', + type: 'integer' + }, + protocol: { + description: + '`protocol` optionally defines a protocol to filter flows by. The available options are `TCP`, `UDP`, `ICMP`, `ICMPv6`, and `SCTP`.', + type: 'string', + enum: ['TCP', 'UDP', 'ICMP', 'ICMPv6', 'SCTP'] + }, + direction: { + description: + '`direction` optionally defines a direction to filter flows by. The available options are `Ingress` and `Egress`.', + type: 'string', + enum: ['Ingress', 'Egress'] + } + } + }, + interfaces: { + description: + '`interfaces` contains 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.', + type: 'array', + items: { + type: 'string' + } + }, + excludeInterfaces: { + description: + '`excludeInterfaces` contains 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.', + type: 'array', + default: ['lo'], + items: { + type: 'string' + } + }, + logLevel: { + description: '`logLevel` defines the log level for the network observability eBPF Agent', + type: 'string', + default: 'info', + enum: ['trace', 'debug', 'info', 'warn', 'error', 'fatal', 'panic'] + }, + imagePullPolicy: { + description: '`imagePullPolicy` is the Kubernetes pull policy for the image defined above', + type: 'string', + default: 'IfNotPresent', + enum: ['IfNotPresent', 'Always', 'Never'] + }, + metrics: { + description: '`metrics` defines the eBPF agent configuration regarding metrics.', + type: 'object', + properties: { + disableAlerts: { + description: + '`disableAlerts` is a list of alerts that should be disabled.\nPossible values are:\n`NetObservDroppedFlows`, which is triggered when the eBPF agent is missing packets or flows, such as when the BPF hashmap is busy or full, or the capacity limiter is being triggered.', + type: 'array', + items: { + description: + 'Name of an eBPF agent alert.\nPossible values are:\n`NetObservDroppedFlows`, which is triggered when the eBPF agent is missing packets or flows, such as when the BPF hashmap is busy or full, or the capacity limiter is being triggered.', + type: 'string', + enum: ['NetObservDroppedFlows'] + } + }, + enable: { + description: + 'Set `enable` to `false` to disable eBPF agent metrics collection. It is enabled by default.', + type: 'boolean' + }, + server: { + description: 'Metrics server endpoint configuration for the 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'] + } + } + } + } + } + } + }, + cacheMaxFlows: { + description: + '`cacheMaxFlows` is 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.', + type: 'integer', + format: 'int32', + default: 100000, + minimum: 1 + }, + cacheActiveTimeout: { + description: + '`cacheActiveTimeout` is 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.', + type: 'string', + default: '5s', + pattern: '^\\d+(ns|ms|s|m)?$' + }, + kafkaBatchSize: { + description: + '`kafkaBatchSize` limits the maximum size of a request in bytes before being sent to a partition. Ignored when not using Kafka. Default: 1MB.', + type: 'integer', + default: 1048576 + }, + 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: '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 + } + } + } + }, + advanced: { + description: + '`advanced` allows setting some aspects of the internal configuration of the eBPF agent.\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: { + 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' + } + }, + 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' + } + } + } + } + } + } + } + } + } + }, + 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 interval 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 interval 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 interval 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 + } + } + } + } + } + } + } + } + } + } +}; \ No newline at end of file diff --git a/web/moduleMapper/templates.ts b/web/moduleMapper/templates.ts new file mode 100644 index 000000000..cff943e05 --- /dev/null +++ b/web/moduleMapper/templates.ts @@ -0,0 +1,197 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { safeYAMLToJS } from '../src/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: any | null = null; +export const GetFlowCollectorJS = (): any => { + 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: any | null = null; +export const GetFlowMetricJS = (): any => { + if (flowMetricJS === null) { + flowMetricJS = safeYAMLToJS(FlowMetricDefaultForm); + } + return flowMetricJS!; +}; diff --git a/web/package-lock.json b/web/package-lock.json index c84ac916a..d001b7775 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -11,12 +11,16 @@ "@jpinsonneau/html-to-image": "^1.11.13", "@patternfly/patternfly": "4.224.2", "@patternfly/react-charts": "6.94.19", + "@patternfly/react-code-editor": "4.82.122", "@patternfly/react-core": "4.276.11", "@patternfly/react-table": "4.113.0", "@patternfly/react-topology": "4.93.6", + "@rjsf/core": "^5.24.12", + "@rjsf/validator-ajv8": "^5.24.12", "axios": "^1.12.0", "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", @@ -40,6 +44,7 @@ "@types/copy-webpack-plugin": "8.0.1", "@types/enzyme": "3.10.x", "@types/jest": "27.0.3", + "@types/js-yaml": "^4.0.9", "@types/jsdom": "^16.2.13", "@types/lodash": "^4.17.20", "@types/murmurhash-js": "^1.0.3", @@ -3124,6 +3129,43 @@ "react-dom": "^16.8 || ^17 || ^18" } }, + "node_modules/@patternfly/react-code-editor": { + "version": "4.82.122", + "resolved": "https://registry.npmjs.org/@patternfly/react-code-editor/-/react-code-editor-4.82.122.tgz", + "integrity": "sha512-6wuf0fxPdX5y7WUT/j2e56breSHkpBQVZOJoZmtnyaz5u8QA0eht6Oh+fibXTJFYAb4uAnUjUtKXJvFXfnaweA==", + "license": "MIT", + "dependencies": { + "@patternfly/react-core": "^4.278.1", + "@patternfly/react-icons": "^4.93.7", + "@patternfly/react-styles": "^4.92.8", + "react-dropzone": "9.0.0", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17 || ^18", + "react-dom": "^16.8 || ^17 || ^18", + "react-monaco-editor": "^0.51.0" + } + }, + "node_modules/@patternfly/react-code-editor/node_modules/@patternfly/react-core": { + "version": "4.278.1", + "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.278.1.tgz", + "integrity": "sha512-BZ+A0r/xLWXLxE5/b8FTVxRI/KokDlTQOS0ub49ts7nv++vmZS7kU4tn2bfuh7RVw/BfW4CNtoMzeJkM8GpaWw==", + "license": "MIT", + "dependencies": { + "@patternfly/react-icons": "^4.93.7", + "@patternfly/react-styles": "^4.92.8", + "@patternfly/react-tokens": "^4.94.7", + "focus-trap": "6.9.2", + "react-dropzone": "9.0.0", + "tippy.js": "5.1.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17 || ^18", + "react-dom": "^16.8 || ^17 || ^18" + } + }, "node_modules/@patternfly/react-core": { "version": "4.276.11", "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.276.11.tgz", @@ -3243,6 +3285,92 @@ "node": ">=14" } }, + "node_modules/@rjsf/core": { + "version": "5.24.13", + "resolved": "https://registry.npmjs.org/@rjsf/core/-/core-5.24.13.tgz", + "integrity": "sha512-ONTr14s7LFIjx2VRFLuOpagL76sM/HPy6/OhdBfq6UukINmTIs6+aFN0GgcR0aXQHFDXQ7f/fel0o/SO05Htdg==", + "license": "Apache-2.0", + "dependencies": { + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "markdown-to-jsx": "^7.4.1", + "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.13", + "resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-5.24.13.tgz", + "integrity": "sha512-rNF8tDxIwTtXzz5O/U23QU73nlhgQNYJ+Sv5BAwQOIyhIE2Z3S5tUiSVMwZHt0julkv/Ryfwi+qsD4FiE5rOuw==", + "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.13", + "resolved": "https://registry.npmjs.org/@rjsf/validator-ajv8/-/validator-ajv8-5.24.13.tgz", + "integrity": "sha512-oWHP7YK581M8I5cF1t+UXFavnv+bhcqjtL1a7MG/Kaffi0EwhgcYjODrD8SsnrhncsEYMqSECr4ZOEoirnEUWw==", + "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/@sinonjs/commons": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", @@ -3903,6 +4031,13 @@ "pretty-format": "^27.0.0" } }, + "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", @@ -4713,7 +4848,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" }, @@ -4730,7 +4864,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", @@ -4745,8 +4878,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", @@ -4886,8 +5018,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", @@ -6410,6 +6541,29 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true }, + "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", @@ -9076,8 +9230,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-glob": { "version": "3.2.12", @@ -9107,6 +9260,22 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fastest-levenshtein": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", @@ -13022,7 +13191,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" }, @@ -13115,6 +13283,31 @@ "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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", @@ -13171,6 +13364,16 @@ "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==", + "license": "MIT", + "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", @@ -13343,6 +13546,12 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -13639,6 +13848,23 @@ "tmpl": "1.0.5" } }, + "node_modules/markdown-to-jsx": { + "version": "7.7.15", + "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.7.15.tgz", + "integrity": "sha512-U5dw5oRajrPTE2oJQWAbLK8RgbCDJ264AjW3fGABq+/rZjQ0E/WGVCLKAHvpKHQFUwoWoK8ZZWVPNLR/biYMhg==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "peerDependencies": { + "react": ">= 0.14.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, "node_modules/marked": { "version": "4.2.12", "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.12.tgz", @@ -14006,6 +14232,13 @@ "url": "https://opencollective.com/mobx" } }, + "node_modules/monaco-editor": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.34.1.tgz", + "integrity": "sha512-FKc80TyiMaruhJKKPz5SpJPIjL+dflGvz4CpuThaPMc94AyN7SeC9HQ8hrvaxX7EyHdJcUY5i4D0gNyJj1vSZQ==", + "license": "MIT", + "peer": true + }, "node_modules/moo": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.1.tgz", @@ -15489,7 +15722,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" } @@ -15746,6 +15978,21 @@ "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17" } }, + "node_modules/react-monaco-editor": { + "version": "0.51.0", + "resolved": "https://registry.npmjs.org/react-monaco-editor/-/react-monaco-editor-0.51.0.tgz", + "integrity": "sha512-6jx1V8p6gHVKJHFaTvicOtmlhFjOJhekobeNd92ZAo7F5UvAin1cF7bxWLCKgtxClYZ7CB3Ar284Kpbhj22FpQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "@types/react": ">=17 <= 18", + "monaco-editor": "^0.34.1", + "react": ">=17 <= 18" + } + }, "node_modules/react-redux": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.2.tgz", @@ -16148,7 +16395,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" } @@ -18356,7 +18602,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" } @@ -18452,6 +18697,44 @@ "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==", + "license": "MIT", + "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", @@ -21939,6 +22222,34 @@ "victory-zoom-container": "^36.6.7" } }, + "@patternfly/react-code-editor": { + "version": "4.82.122", + "resolved": "https://registry.npmjs.org/@patternfly/react-code-editor/-/react-code-editor-4.82.122.tgz", + "integrity": "sha512-6wuf0fxPdX5y7WUT/j2e56breSHkpBQVZOJoZmtnyaz5u8QA0eht6Oh+fibXTJFYAb4uAnUjUtKXJvFXfnaweA==", + "requires": { + "@patternfly/react-core": "^4.278.1", + "@patternfly/react-icons": "^4.93.7", + "@patternfly/react-styles": "^4.92.8", + "react-dropzone": "9.0.0", + "tslib": "^2.0.0" + }, + "dependencies": { + "@patternfly/react-core": { + "version": "4.278.1", + "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.278.1.tgz", + "integrity": "sha512-BZ+A0r/xLWXLxE5/b8FTVxRI/KokDlTQOS0ub49ts7nv++vmZS7kU4tn2bfuh7RVw/BfW4CNtoMzeJkM8GpaWw==", + "requires": { + "@patternfly/react-icons": "^4.93.7", + "@patternfly/react-styles": "^4.92.8", + "@patternfly/react-tokens": "^4.94.7", + "focus-trap": "6.9.2", + "react-dropzone": "9.0.0", + "tippy.js": "5.1.2", + "tslib": "^2.0.0" + } + } + } + }, "@patternfly/react-core": { "version": "4.276.11", "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.276.11.tgz", @@ -22029,6 +22340,67 @@ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.7.0.tgz", "integrity": "sha512-Eu1V3kz3mV0wUpVTiFHuaT8UD1gj/0VnoFHQYX35xlslQUpe8CuYoKFn9d4WZFHm3yDywz6ALZuGdnUPKrNeAw==" }, + "@rjsf/core": { + "version": "5.24.13", + "resolved": "https://registry.npmjs.org/@rjsf/core/-/core-5.24.13.tgz", + "integrity": "sha512-ONTr14s7LFIjx2VRFLuOpagL76sM/HPy6/OhdBfq6UukINmTIs6+aFN0GgcR0aXQHFDXQ7f/fel0o/SO05Htdg==", + "requires": { + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "markdown-to-jsx": "^7.4.1", + "prop-types": "^15.8.1" + } + }, + "@rjsf/utils": { + "version": "5.24.13", + "resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-5.24.13.tgz", + "integrity": "sha512-rNF8tDxIwTtXzz5O/U23QU73nlhgQNYJ+Sv5BAwQOIyhIE2Z3S5tUiSVMwZHt0julkv/Ryfwi+qsD4FiE5rOuw==", + "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.13", + "resolved": "https://registry.npmjs.org/@rjsf/validator-ajv8/-/validator-ajv8-5.24.13.tgz", + "integrity": "sha512-oWHP7YK581M8I5cF1t+UXFavnv+bhcqjtL1a7MG/Kaffi0EwhgcYjODrD8SsnrhncsEYMqSECr4ZOEoirnEUWw==", + "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==" + } + } + }, "@sinonjs/commons": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", @@ -22663,6 +23035,12 @@ "pretty-format": "^27.0.0" } }, + "@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", @@ -23334,7 +23712,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" }, @@ -23343,7 +23720,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", @@ -23354,8 +23730,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==" } } }, @@ -23450,8 +23825,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", @@ -24645,6 +25019,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", @@ -26766,8 +27163,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-glob": { "version": "3.2.12", @@ -26794,6 +27190,11 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==" + }, "fastest-levenshtein": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", @@ -29717,7 +30118,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" } @@ -29788,6 +30188,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", @@ -29835,6 +30255,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", @@ -29960,6 +30386,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -30193,6 +30624,12 @@ "tmpl": "1.0.5" } }, + "markdown-to-jsx": { + "version": "7.7.15", + "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.7.15.tgz", + "integrity": "sha512-U5dw5oRajrPTE2oJQWAbLK8RgbCDJ264AjW3fGABq+/rZjQ0E/WGVCLKAHvpKHQFUwoWoK8ZZWVPNLR/biYMhg==", + "requires": {} + }, "marked": { "version": "4.2.12", "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.12.tgz", @@ -30452,6 +30889,12 @@ "resolved": "https://registry.npmjs.org/mobx/-/mobx-5.15.7.tgz", "integrity": "sha512-wyM3FghTkhmC+hQjyPGGFdpehrcX1KOXsDuERhfK2YbJemkUhEB+6wzEN639T21onxlfYBmriA1PFnvxTUhcKw==" }, + "monaco-editor": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.34.1.tgz", + "integrity": "sha512-FKc80TyiMaruhJKKPz5SpJPIjL+dflGvz4CpuThaPMc94AyN7SeC9HQ8hrvaxX7EyHdJcUY5i4D0gNyJj1vSZQ==", + "peer": true + }, "moo": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.1.tgz", @@ -31562,8 +32005,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==" }, "qs": { "version": "6.13.0", @@ -31754,6 +32196,15 @@ "warning": "^4.0.3" } }, + "react-monaco-editor": { + "version": "0.51.0", + "resolved": "https://registry.npmjs.org/react-monaco-editor/-/react-monaco-editor-0.51.0.tgz", + "integrity": "sha512-6jx1V8p6gHVKJHFaTvicOtmlhFjOJhekobeNd92ZAo7F5UvAin1cF7bxWLCKgtxClYZ7CB3Ar284Kpbhj22FpQ==", + "peer": true, + "requires": { + "prop-types": "^15.8.1" + } + }, "react-redux": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.2.tgz", @@ -32073,8 +32524,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", @@ -33776,7 +34226,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" } @@ -33862,6 +34311,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 ce1b68946..cbd40689c 100644 --- a/web/package.json +++ b/web/package.json @@ -3,8 +3,10 @@ "version": "0.0.0", "private": true, "scripts": { - "clean": "rm -rf ./dist", + "clean": "rm -rf ./dist && ./manage-extension.sh", + "clean:static": "rm -rf ./dist/static && ./manage-extension.sh 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.ts", "start": "npm run ts-node-transpile ./node_modules/.bin/webpack serve", @@ -38,6 +40,7 @@ "@types/copy-webpack-plugin": "8.0.1", "@types/enzyme": "3.10.x", "@types/jest": "27.0.3", + "@types/js-yaml": "^4.0.9", "@types/jsdom": "^16.2.13", "@types/lodash": "^4.17.20", "@types/murmurhash-js": "^1.0.3", @@ -88,30 +91,20 @@ "webpack-dev-server": "^4.6.0", "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": "*" - } - }, "dependencies": { "@jpinsonneau/html-to-image": "^1.11.13", "@patternfly/patternfly": "4.224.2", "@patternfly/react-charts": "6.94.19", + "@patternfly/react-code-editor": "4.82.122", "@patternfly/react-core": "4.276.11", "@patternfly/react-table": "4.113.0", "@patternfly/react-topology": "4.93.6", + "@rjsf/core": "^5.24.12", + "@rjsf/validator-ajv8": "^5.24.12", "axios": "^1.12.0", "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", @@ -166,4 +159,4 @@ ], "resolver": "ts-jest-resolver" } -} +} \ No newline at end of file diff --git a/web/src/app.tsx b/web/src/app.tsx index 33354a764..309bc08f0 100755 --- a/web/src/app.tsx +++ b/web/src/app.tsx @@ -14,6 +14,11 @@ import { } from '@patternfly/react-core'; import React from 'react'; import { BrowserRouter, Link, Route, Routes } from 'react-router-dom'; +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 NetflowTrafficTab from './components/netflow-traffic-tab'; @@ -48,6 +53,26 @@ export const pages = [ { id: 'udn-tab', name: 'UDN tab' + }, + { + id: 'flowCollector-wizard', + name: 'FlowCollector wizard' + }, + { + id: 'flowCollector', + name: 'FlowCollector form' + }, + { + id: 'flowCollector-status', + name: 'FlowCollector status' + }, + { + id: 'flowMetric-wizard', + name: 'FlowMetric wizard' + }, + { + id: 'flowMetric', + name: 'FlowMetric form' } ]; @@ -102,6 +127,16 @@ export class App extends React.Component<{}, AppState> { return ( ); + case 'flowCollector-wizard': + return ; + case 'flowCollector': + return ; + case 'flowCollector-status': + return ; + case 'flowMetric-wizard': + return ; + case 'flowMetric': + return ; default: return ; } diff --git a/web/src/components/dynamic-loader/dynamic-loader.tsx b/web/src/components/dynamic-loader/dynamic-loader.tsx index 89349678b..03415d82a 100644 --- a/web/src/components/dynamic-loader/dynamic-loader.tsx +++ b/web/src/components/dynamic-loader/dynamic-loader.tsx @@ -9,6 +9,7 @@ import * as reactRouterDom from 'react-router-dom'; */ type NavFunc = (to: string, opts?: any) => void; let navFunc: NavFunc | null = null; +let backFunc: (() => void) | null = null; export const loadNavFunction = () => { const genericReactRouterDom = reactRouterDom as any; @@ -18,6 +19,7 @@ export const loadNavFunction = () => { } else if (genericReactRouterDom['useHistory']) { console.log('loading nav function from react-router useHistory'); setNavFunction(genericReactRouterDom['useHistory']().push); + setBackFunction(genericReactRouterDom['useHistory']().goBack); } else { console.error("can't load nav function from react-router"); } @@ -27,6 +29,10 @@ export const setNavFunction = (f: NavFunc) => { navFunc = f; }; +export const setBackFunction = (f: () => void) => { + backFunc = f; +}; + export const navigate = (to: string, opts?: any) => { if (!navFunc) { loadNavFunction(); @@ -39,6 +45,18 @@ export const navigate = (to: string, opts?: any) => { } }; +export const back = () => { + if (!backFunc) { + loadNavFunction(); + } + + if (backFunc) { + backFunc(); + } else { + console.error('back error; backFunc is not initialized', backFunc); + } +}; + export const DynamicLoader: React.FC<{}> = ({ children }) => { loadNavFunction(); return <>{!!children ? children : ''}; diff --git a/web/src/components/forms/config/uiSchema.ts b/web/src/components/forms/config/uiSchema.ts new file mode 100644 index 000000000..d0ffc10f0 --- /dev/null +++ b/web/src/components/forms/config/uiSchema.ts @@ -0,0 +1,1813 @@ +/* eslint-disable max-len */ +import { UiSchema } from '@rjsf/utils'; + +// Keep the UISchemas ordered for form display + +export const FlowCollectorUISchema: UiSchema = { + 'ui:title': 'FlowCollector', + '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' + }, + kafka: { + 'ui:title': 'Kafka configuration', + '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', + 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', + type: { + 'ui:widget': 'hidden' + }, + ipfix: { + 'ui:widget': 'hidden' + }, + ebpf: { + 'ui:title': 'eBPF Agent configuration', + '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: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', + 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' + }, + excludeInterfaces: { + 'ui:title': 'Exclude interfaces' + }, + logLevel: { + 'ui:title': 'Log level' + }, + imagePullPolicy: { + 'ui:title': 'Image pull policy' + }, + metrics: { + 'ui:title': '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' + }, + cacheActiveTimeout: { + 'ui:title': 'Cache active timeout' + }, + kafkaBatchSize: { + 'ui:title': 'Kafka batch size', + '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', + filters: { + 'ui:title': 'Filters', + 'ui:widget': 'hidden', + items: { + 'ui:order': ['allOf', 'outputTarget', 'sampling', '*'], + allOf: { + items: { + 'ui:order': ['field', 'matchType', 'value', '*'] + } + } + } + }, + multiClusterDeployment: { + 'ui:title': 'Multi-cluster deployment' + }, + clusterName: { + 'ui:title': 'Cluster name', + 'ui:dependency': { + controlFieldPath: ['processor', 'multiClusterDeployment'], + controlFieldValue: 'true', + controlFieldName: 'multiClusterDeployment' + } + }, + addZone: { + 'ui:title': 'Availability zones' + }, + subnetLabels: { + 'ui:title': 'Subnet labels', + openShiftAutoDetect: { + 'ui:widget': 'hidden' + }, + customLabels: { + 'ui:title': 'Custom labels', + items: { + 'ui:order': ['cidrs', 'name', '*'] + } + }, + 'ui:order': ['openShiftAutoDetect', 'customLabels', '*'] + }, + logTypes: { + 'ui:title': 'Log types', + 'ui:widget': 'hidden' + }, + logLevel: { + 'ui:title': 'Log level' + }, + imagePullPolicy: { + 'ui:title': 'Image pull policy' + }, + deduper: { + 'ui:title': 'Deduper', + 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', + 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', '*'] + }, + includeList: { + 'ui:title': 'Include list' + }, + alerts: { + 'ui:title': 'Alerts' + }, + disableAlerts: { + 'ui:title': 'Disable alerts' + }, + 'ui:order': ['server', 'includeList', 'alerts', 'disableAlerts', '*'] + }, + 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' + }, + mode: { + 'ui:title': 'Mode' + }, + manual: { + 'ui:title': 'Manual', + '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:order': ['enable', 'mode', 'manual', 'timeout', '*'] + } + }, + loki: { + 'ui:title': 'Loki client settings', + 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:widget': 'hidden' + }, + writeTimeout: { + 'ui:title': 'Write timeout', + 'ui:widget': 'hidden' + }, + writeBatchWait: { + 'ui:title': 'Write batch wait', + 'ui:widget': 'hidden' + }, + writeBatchSize: { + 'ui:title': 'Write batch size', + 'ui:widget': 'hidden' + }, + advanced: { + 'ui:title': 'Advanced configuration', + 'ui:widget': 'hidden', + staticLabels: { + 'ui:title': 'Static labels' + }, + writeMaxRetries: { + 'ui:title': 'Write max retries' + }, + writeMaxBackoff: { + 'ui:title': 'Write max backoff' + }, + writeMinBackoff: { + 'ui:title': 'Write min backoff' + }, + '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', + enable: { + 'ui:title': 'Deploy console plugin' + }, + logLevel: { + 'ui:title': 'Log level' + }, + imagePullPolicy: { + 'ui:title': 'Image pull policy' + }, + portNaming: { + 'ui:title': 'Port naming', + '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:widget': 'hidden', + items: { + 'ui:order': ['filter', 'name', 'default', '*'] + } + }, + replicas: { + 'ui:title': 'Replicas' + }, + 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: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', + enable: { + 'ui:title': 'Deploy policies' + }, + additionalNamespaces: { + 'ui:title': 'Additional namespaces', + 'ui:dependency': { + controlFieldPath: ['networkPolicy', 'enable'], + controlFieldValue: 'true', + controlFieldName: 'enable' + } + }, + 'ui:order': ['enable', 'additionalNamespaces', '*'] + }, + exporters: { + 'ui:title': 'Exporters', + items: { + type: { + 'ui:title': 'Type' + }, + 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', + 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:flat': 'true', + metadata: { + 'ui:title': 'Metadata', + 'ui:flat': 'true', + name: { + 'ui:title': 'Name' + }, + namespace: { + 'ui:title': 'Namespace', + 'ui:description': 'It must match the one configured in FlowCollector.' + }, + labels: { + 'ui:widget': 'hidden' + }, + 'ui:order': ['name', 'namespace', 'labels', '*'] + }, + spec: { + metricName: { + 'ui:title': 'Metric name' + }, + type: { + 'ui:title': 'Type' + }, + buckets: { + 'ui:title': 'Buckets', + 'ui:dependency': { + controlFieldPath: ['type'], + controlFieldValue: 'Histogram', + controlFieldName: 'type' + } + }, + valueField: { + 'ui:title': 'Value field' + }, + divider: { + 'ui:title': 'Divider' + }, + labels: { + 'ui:title': 'Labels' + }, + flatten: { + 'ui:title': 'Flatten' + }, + remap: { + 'ui:title': 'Remap', + 'ui:widget': 'map' + }, + direction: { + 'ui:title': 'Direction' + }, + filters: { + 'ui:title': 'Filters', + items: { + field: { + 'ui:title': 'Field' + }, + matchType: { + 'ui:title': 'Match type' + }, + value: { + 'ui:title': 'Value' + }, + 'ui:order': ['field', 'matchType', 'value', '*'] + } + }, + charts: { + 'ui:title': 'Charts', + items: { + dashboardName: { + 'ui:title': 'Dashboard name' + }, + sectionName: { + 'ui:title': 'Section name' + }, + title: { + 'ui:title': 'Title' + }, + unit: { + 'ui:title': 'Unit' + }, + type: { + 'ui:title': 'Type' + }, + queries: { + 'ui:title': 'Queries', + items: { + promQL: { + 'ui:title': 'Query' + }, + legend: { + 'ui:title': 'Legend' + }, + top: { + 'ui:title': 'Top' + }, + '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..93b45cdbe --- /dev/null +++ b/web/src/components/forms/consumption.tsx @@ -0,0 +1,226 @@ +import { PrometheusEndpoint, PrometheusResponse, usePrometheusPoll } from '@openshift-console/dynamic-plugin-sdk'; +import { Flex, FlexItem, Slider, Spinner, Text, TextVariants } from '@patternfly/react-core'; +import { InfoAltIcon, WarningTriangleIcon } from '@patternfly/react-icons'; +import { Table, TableVariant, 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 = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + flowCollector: any | 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 as number) || 50; + }, [flowCollector?.spec?.agent?.ebpf?.sampling]); + + const getSamplings = React.useCallback(() => { + const current = getCurrentSampling(); + let samplings = [1, 25, 50, 100, 500, 1000]; + if (!samplings.includes(current)) { + samplings.push(current); + samplings = _.sortBy(samplings); + } + return samplings; + }, [getCurrentSampling]); + + const getSamplingIndex = React.useCallback(() => { + const current = getCurrentSampling(); + const idx = getSamplings().indexOf(current); + if (idx < 0) { + return 0; + } + return idx; + }, [getSamplings, 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')} + + {t( + // eslint-disable-next-line max-len + 'The example outlined in the table demonstrates a scenario that is tailored to your workload. Consider this example only as a baseline from which adjustments can be made to accommodate your needs.' + )} + + + + + + + + + + + + {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('Performance tuning and estimation')} + + {t( + // eslint-disable-next-line max-len + 'The sampling interval is one of the main settings used to balance performance and accuracy. The lower the interval, the higher the accuracy.' + )} +
+ {t('Use the slider below to configure the desired sampling interval.')} +
+ ({ value: i, label: String(s) }))} + max={getSamplings().length - 1} + onChange={(_, value) => { + setSampling(getSamplings()[value || 1]); + }} + /> + + + + + + + + + + {getSamplings().map((sampling, i) => { + const current = getCurrentSampling() === sampling; + const estimate = getEstimation(sampling); + return ( + setSampling && setSampling(sampling)} + > + + + + + ); + })} + +
{t('Sampling interval')}{t('vCPU')}{t('Memory')}
{sampling}{`${estimate.cpu}vCPUs`}{`${estimate.memory}GiB`}
+ + {' '} + {t( + // eslint-disable-next-line max-len + 'The estimations are based on the number of nodes in the cluster and the sampling rate. They do not take into account the number of namespaces or pods, as their impact is comparatively lower than that of nodes.\nThey are calculated using a linear regression model based on data collected from various OpenShift clusters. Actual resource consumption may vary depending on your specific workload and cluster configuration.' + )} + +
+
+ ); +}; + +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..3388cbaaa --- /dev/null +++ b/web/src/components/forms/dynamic-form/const.ts @@ -0,0 +1,33 @@ +export const K8sUISchema = { + 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 jsonSchemaGroupTypes: string[] = ['object', 'array']; +export const jsonSchemaNumberTypes: 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..d80ead8c6 --- /dev/null +++ b/web/src/components/forms/dynamic-form/dynamic-form.css @@ -0,0 +1,51 @@ +.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; +} + +.checkboxes-container { + padding: 1rem; +} + +pre.backticks { + display: inline; +} + +#open-flow-collector-form, #open-flow-metrics-form { + padding: 0; +} 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..01f3042fe --- /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 { K8sUISchema } 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({}, K8sUISchema, 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..fd7db0510 --- /dev/null +++ b/web/src/components/forms/dynamic-form/fields.tsx @@ -0,0 +1,164 @@ +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(); + const { t } = useTranslation('plugin__netobserv-plugin'); + + const formatText = React.useCallback((rawText: string) => { + const tokenized = rawText.replaceAll(/(`[a-zA-Z0-9_.]+`)/g, '@@@$1@@@'); + const tokens = tokenized.split('@@@'); + return tokens.map(t => { + if (t === '') { + return null; + } + if (t.startsWith('`') && t.endsWith('`') && t.length > 2) { + return
{t.substring(1, t.length - 1)}
; + } + return t; + }); + }, []); + + if (!description) { + return null; + } + + const desc = description.replaceAll('
', ''); + const parts = desc.split('\n'); + let content = <>{formatText(desc)}; + 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..204183236 --- /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 { jsonSchemaGroupTypes } 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 = jsonSchemaGroupTypes.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..169ba5138 --- /dev/null +++ b/web/src/components/forms/dynamic-form/utils.spec.ts @@ -0,0 +1,57 @@ +import { prune } from './utils'; + +const pruneData = { + abc: { + '123': {} + }, + test1: { + num: NaN, + str: '', + bool: null + }, + test2: { + num: NaN, + str: '', + bool: null + }, + test3: { + child: { + grandchild: {} + } + }, + test4: { + arr1: [NaN, '', undefined, null, {}], + arr2: [] + } +}; + +const pruneSample = { + test2: {}, + test3: { + child: {} + }, + test4: { + arr1: [] + } +}; + +describe('prune', () => { + it('Prunes all empty data when no sample is provided', () => { + const result = prune(pruneData); + 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(pruneData, pruneSample); + 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..f63b2121c --- /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 unsupportedSchemaProperties = ['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), unsupportedSchemaProperties), 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..6db6ef18b --- /dev/null +++ b/web/src/components/forms/dynamic-form/widgets.tsx @@ -0,0 +1,219 @@ +import { CodeEditor, Language } from '@patternfly/react-code-editor'; +import { Checkbox, Dropdown, DropdownItem, DropdownToggle, Flex, FlexItem, 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 { jsonSchemaNumberTypes } 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 jsonSchemaNumberTypes.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('True')} + labelOff={t('False')} + /> + ); +}; + +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))} + onFocus={onFocus && (() => onFocus(id, value))} + toggle={ + setIsOpen(!isOpen)} onToggle={() => setIsOpen(!isOpen)}> + {value || t('Select {{title}}', { title: title || schema?.title || label })} + + } + > + {enumOptions.map(option => ( + { + onChange(option.value, undefined, id); + setIsOpen(false); + }} + > + {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 + }} + > + 4 ? 'row' : 'column' }} + onBlur={() => 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..293e836bf --- /dev/null +++ b/web/src/components/forms/editor-toggle.tsx @@ -0,0 +1,132 @@ +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; + onDelete: () => void; + customChild: JSX.Element; + yamlChild: JSX.Element; + updated: boolean; + isUpdate: boolean; + onReload: () => void; +}; + +export const EditorToggle: FC = ({ + type, + onChange, + onSubmit, + onCancel, + onReload, + onDelete, + 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 && ( + + )} + + {isUpdate && ( + + )} + + + )} + + ); +}; diff --git a/web/src/components/forms/flowCollector-status.tsx b/web/src/components/forms/flowCollector-status.tsx new file mode 100644 index 000000000..85ee4fe63 --- /dev/null +++ b/web/src/components/forms/flowCollector-status.tsx @@ -0,0 +1,94 @@ +import React, { FC } from 'react'; + +import { Button, Flex, FlexItem, PageSection, TextContent, Title } from '@patternfly/react-core'; +import { useTranslation } from 'react-i18next'; +import { flowCollectorEditPath, flowCollectorNewPath, netflowTrafficPath } from '../../utils/url'; +import DynamicLoader, { navigate } from '../dynamic-loader/dynamic-loader'; +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 = () => { + const { t } = useTranslation('plugin__netobserv-plugin'); + const [selectedTypes, setSelectedTypes] = React.useState([]); + + return ( + + + + {ctx => { + return ( + + + {ctx.data && ( + + + + + + + + + + + + + + + + + + + )} + {ctx.loadError && ( + + + + {t('An error occured while retreiving FlowCollector: {{error}}', { error: ctx.loadError })} + + + + + + + )} + + ); + }} + + + + ); +}; + +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..11e69a53f --- /dev/null +++ b/web/src/components/forms/flowCollector-wizard.tsx @@ -0,0 +1,203 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Button, PageSection, Title, Wizard } from '@patternfly/react-core'; +import { RJSFSchema } from '@rjsf/utils'; +import validator from '@rjsf/validator-ajv8'; +import _ from 'lodash'; +import React, { FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; +import { flowCollectorEditPath, flowCollectorNewPath, flowCollectorStatusPath } from '../../utils/url'; +import DynamicLoader, { navigate } from '../dynamic-loader/dynamic-loader'; +import { FlowCollectorUISchema } from './config/uiSchema'; +import Consumption from './consumption'; +import { DynamicForm } from './dynamic-form/dynamic-form'; +import { ErrorTemplate } from './dynamic-form/templates'; +import './forms.css'; +import ResourceWatcher, { Consumer } from './resource-watcher'; +import { getFilteredUISchema } from './utils'; + +export type FlowCollectorWizardProps = { + name?: string; +}; + +const defaultPaths = ['spec.namespace', 'spec.networkPolicy']; + +export const FlowCollectorWizard: FC = props => { + const { t } = useTranslation('plugin__netobserv-plugin'); + const [schema, setSchema] = React.useState(null); + const [data, setData] = React.useState(null); + const [paths, setPaths] = React.useState(defaultPaths); + const params = useParams(); + + const form = React.useCallback( + (errors?: string[]) => { + if (!schema) { + return <>; + } + const filteredSchema = getFilteredUISchema(FlowCollectorUISchema, paths); + return ( + { + setData(event.formData); + }} + errors={errors} + /> + ); + }, + [data, paths, schema] + ); + + const onStepChange = (newStep: { id?: string | number; name: React.ReactNode }) => { + switch (newStep.id) { + case 'overview': + setPaths(defaultPaths); + break; + case 'processing': + setPaths([ + 'spec.deploymentModel', + 'spec.kafka.address', + 'spec.kafka.topic', + 'spec.kafka.tls', + 'spec.agent.ebpf.privileged', + 'spec.agent.ebpf.features', + 'spec.processor.clusterName', + 'spec.processor.addZone' + ]); + break; + case 'loki': + setPaths(['spec.loki']); + 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 => { + // redirect to edit page if resource already exists or is created while using the wizard + // We can't handle edition here since this page doesn't include ResourceYAMLEditor + // which handle reload / update buttons + if (ctx.data.metadata?.resourceVersion) { + navigate(flowCollectorEditPath); + } + // first init schema & data when watch resource query got results + if (schema == null) { + setSchema(ctx.schema); + } + if (data == null) { + // slightly modify default example when creating a new resource + if (params.name !== 'cluster') { + const updatedData = _.cloneDeep(ctx.data) as any; + if (!updatedData.spec) { + updatedData.spec = {}; + } + if (!updatedData.spec.loki) { + updatedData.spec.loki = {}; + } + updatedData.spec.loki.mode = 'LokiStack'; // default to lokistack + setData(updatedData); + } else { + setData(ctx.data); + } + } + return ( + + +
+ + + {t( + // eslint-disable-next-line max-len + 'The FlowCollector resource is used to configure the Network Observability operator and its managed components. When it is created, network flows start being collected.' + )} +

+ {t( + // eslint-disable-next-line max-len + 'This wizard is a helper to create a first FlowCollector resource. It does not cover all the available configuration options, but only the most common ones.\nFor advanced configuration, please use YAML or the' + )}{' '} + + {t( + // eslint-disable-next-line max-len + ', which includes more options such as:\n- Filtering options\n- Configuring custom exporters\n- Custom labels based on IP\n- Pod identification for secondary networks\n- Performance fine-tuning\nYou can always edit a FlowCollector later when you start with the simplified configuration.' + )} +

+ {t('Operator configuration')} +
+ {form(ctx.errors)} + + ) + }, + { id: 'processing', name: t('Processing'), component: form(ctx.errors) }, + { id: 'loki', name: t('Loki'), component: form(ctx.errors) }, + { + id: 'consumption', + name: t('Consumption'), + component: ( + <> + + <>{!_.isEmpty(ctx.errors) && } + + ), + nextButtonText: t('Submit') + } + ]} + onGoToStep={onStepChange} + onSave={() => ctx.onSubmit(data)} + onClose={() => navigate('/')} + /> +
+
+ ); + }} +
+
+
+ ); +}; + +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..ead947664 --- /dev/null +++ b/web/src/components/forms/flowCollector.tsx @@ -0,0 +1,33 @@ +import React, { FC } from 'react'; + +import { useParams } from 'react-router-dom'; +import DynamicLoader, { back } from '../dynamic-loader/dynamic-loader'; +import { FlowCollectorUISchema } from './config/uiSchema'; +import { ResourceForm } from './resource-form'; +import { ResourceWatcher } from './resource-watcher'; + +export type FlowCollectorFormProps = { + name?: string; +}; + +export const FlowCollectorForm: FC = props => { + const params = useParams(); + + return ( + + { + back(); + }} + > + + + + ); +}; + +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..086e76da4 --- /dev/null +++ b/web/src/components/forms/flowMetric-wizard.tsx @@ -0,0 +1,174 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { ResourceYAMLEditor } from '@openshift-console/dynamic-plugin-sdk'; +import { Button, PageSection, Title, Wizard } from '@patternfly/react-core'; +import { RJSFSchema } from '@rjsf/utils'; +import validator from '@rjsf/validator-ajv8'; +import _ from 'lodash'; +import React, { FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; +import { ContextSingleton } from '../../utils/context'; +import { flowMetricNewPath } from '../../utils/url'; +import { safeYAMLToJS } from '../../utils/yaml'; +import DynamicLoader, { back, navigate } from '../dynamic-loader/dynamic-loader'; +import { FlowMetricUISchema } from './config/uiSchema'; +import { DynamicForm } from './dynamic-form/dynamic-form'; +import { ErrorTemplate } from './dynamic-form/templates'; +import './forms.css'; +import ResourceWatcher, { Consumer } from './resource-watcher'; +import { getFilteredUISchema } from './utils'; + +export type FlowMetricWizardProps = { + name?: string; +}; + +const defaultPaths = ['metadata.namespace', 'metadata.name']; + +export const FlowMetricWizard: FC = props => { + const { t } = useTranslation('plugin__netobserv-plugin'); + const [schema, setSchema] = React.useState(null); + + const [data, setData] = React.useState(null); + const [paths, setPaths] = React.useState(defaultPaths); + const params = useParams(); + + const form = React.useCallback( + (errors?: string[]) => { + if (!schema) { + return <>; + } + const filteredSchema = getFilteredUISchema(FlowMetricUISchema, paths); + return ( + { + setData(event.formData); + }} + errors={errors} + /> + ); + }, + [data, paths, schema] + ); + + const onStepChange = (newStep: { id?: string | number; name: React.ReactNode }) => { + switch (newStep.id) { + case 'overview': + setPaths(defaultPaths); + break; + case 'metric': + setPaths(['spec.metricName', 'spec.type', 'spec.valueField', 'spec.labels', 'spec.buckets']); + break; + case 'data': + setPaths(['spec.remap', 'spec.direction', 'spec.filters']); + break; + default: + setPaths([]); + } + }; + + return ( + + { + back(); + }} + ignoreCSVExample={true} + > + + {ctx => { + // first init schema & data when watch resource query got results + if (schema == null) { + setSchema(ctx.schema); + } + if (data == null) { + setData(ctx.data); + } + return ( + + +
+ + + {t( + // eslint-disable-next-line max-len + 'You can create custom metrics out of the network flows using the FlowMetric API. A FlowCollector resource must be created as well in order to produce the flows. Each flow consists in a set of fields with values, such as source name and destination name. These fields can be leveraged as Prometheus labels to enable customized metrics and dashboards.' + )} +
+
+ {t( + // eslint-disable-next-line max-len + 'This simplified setup guides you through the common aspects of the FlowMetric configuration. For advanced configuration, please use YAML or the ' + )} + + {'.'} +

+ {t('Resource configuration')} +
+ {form(ctx.errors)} + + ) + }, + { id: 'metric', name: t('Metric'), component: form(ctx.errors) }, + { id: 'data', name: t('Data'), component: form(ctx.errors) }, + { + id: 'review-step', + name: t('Review'), + component: ( + <> + { + const updatedData = safeYAMLToJS(content); + setData(updatedData); + ctx.onSubmit(updatedData); + }} + /> + <>{!_.isEmpty(ctx.errors) && } + + ), + isFinishedStep: !ContextSingleton.isStandalone() + } + ]} + onGoToStep={onStepChange} + onSave={() => ctx.onSubmit(data)} + onClose={() => navigate('/')} + /> +
+
+ ); + }} +
+
+
+ ); +}; + +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..51f9e0e35 --- /dev/null +++ b/web/src/components/forms/flowMetric.tsx @@ -0,0 +1,34 @@ +import React, { FC } from 'react'; + +import { useParams } from 'react-router-dom'; +import DynamicLoader, { back } from '../dynamic-loader/dynamic-loader'; +import { FlowMetricUISchema } from './config/uiSchema'; +import { ResourceForm } from './resource-form'; +import { ResourceWatcher } from './resource-watcher'; + +export type FlowMetricFormProps = { + name?: string; +}; + +export const FlowMetricForm: FC = props => { + const params = useParams(); + + return ( + + { + back(); + }} + > + + + + ); +}; + +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..6051d551c --- /dev/null +++ b/web/src/components/forms/forms.css @@ -0,0 +1,75 @@ +#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: 1.5rem; +} + +.status-list-container { + overflow-y: auto; +} + +.calculator-item { + padding-bottom: 1.5rem; +} + +#wizard-container { + flex: 1; +} + +.no-padding { + padding: 0; +} + +#estimation-table tr.pf-m-selected td { + font-weight: 700; +} diff --git a/web/src/components/forms/pipeline.tsx b/web/src/components/forms/pipeline.tsx new file mode 100644 index 000000000..a086e0f65 --- /dev/null +++ b/web/src/components/forms/pipeline.tsx @@ -0,0 +1,297 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { + DefaultTaskGroup as taskGroup, + DEFAULT_EDGE_TYPE as edgeType, + DEFAULT_FINALLY_NODE_TYPE as finallyNodeType, + DEFAULT_SPACER_NODE_TYPE as spacerNodeType, + DEFAULT_TASK_NODE_TYPE as taskNodeType, + DEFAULT_WHEN_OFFSET as whenOffset, + FinallyNode, + getEdgesFromNodes, + getSpacerNodes, + Graph, + GraphComponent, + GRAPH_LAYOUT_END_EVENT as layoutEndEvent, + 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 taskNodeType: + return StepNode; + case finallyNodeType: + return FinallyNode; + case 'task-group': + return taskGroup; + case 'finally-group': + return taskGroup; + case spacerNodeType: + return SpacerNode; + case 'finally-spacer-edge': + case edgeType: + return TaskEdge; + default: + return undefined; + } +}; + +export type FlowCollectorPipelineProps = { + existing: any | 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: any | null = existing?.status?.conditions?.find((condition: any) => condition.type === type); + if (condition?.status !== status && condition?.reason !== 'Unused') { + 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[] = []; + + const overallTypes = ['Ready']; + const overallStatus = getStatus(overallTypes, 'True'); + if (existing?.spec?.agent?.type === 'eBPF') { + steps.push({ + id: 'ebpf', + label: 'eBPF agents', + data: { + status: overallStatus, + selected: _.some(selectedTypes, t => overallTypes.includes(t)), + onSelect: () => setSelectedTypes(overallTypes) + } + }); + } + + const flpStatuses = ['WaitingFLPParent', 'WaitingFLPMonolith']; + if (existing?.spec?.deploymentModel === 'Kafka') { + const types = ['WaitingFLPTransformer']; + steps.push({ + id: 'kafka', + label: 'Kafka', + runAfterTasks: ['ebpf'], + data: { + status: getStatus(types, '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, '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: { + status: overallStatus, + selected: _.some(selectedTypes, t => overallTypes.includes(t)), + onSelect: () => setSelectedTypes(overallTypes) + } + }); + } + + return steps.map(s => ({ + type: s.type || taskNodeType, + 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; + } else if (controller.hasGraph()) { + controller.getElements().forEach(e => e.getType() !== 'graph' && controller.removeElement(e)); + controller.getGraph().destroy(); + } + + setTimeout(() => { + 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 + ); + + //TODO: find a smoother way to fit while elements are still moving + setTimeout(fit, 100); + setTimeout(fit, 250); + setTimeout(fit, 500); + }, 500); + }, [controller, fit, 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(layoutEndEvent, 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..9b38b4d7e --- /dev/null +++ b/web/src/components/forms/resource-form.tsx @@ -0,0 +1,151 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { ResourceYAMLEditor } from '@openshift-console/dynamic-plugin-sdk'; +import { Button, FormHelperText, PageSection, Text, TextContent, TextVariants, 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 { safeYAMLToJS } from '../../utils/yaml'; +import { back } from '../dynamic-loader/dynamic-loader'; +import Modal from '../modals/modal'; +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 = { + uiSchema: UiSchema; +}; + +export const ResourceForm: FC = ({ uiSchema }) => { + const { t } = useTranslation('plugin__netobserv-plugin'); + const [viewType, setViewType] = React.useState(EditorType.CUSTOM); + const [data, setData] = React.useState(null); + const [isOpen, setOpen] = React.useState(false); + + const hasChanged = React.useCallback( + (existing: any) => { + return data?.metadata?.resourceVersion !== existing?.metadata?.resourceVersion; + }, + [data?.metadata?.resourceVersion] + ); + + return ( + + {ctx => { + const isFlowCollector = ctx.kind === 'FlowCollector'; + // first init data when watch resource query got results + if (data == null) { + setData(ctx.data); + } + return ( + + + }> + setData(ctx.data)} + onChange={type => { + setViewType(type); + }} + onSubmit={() => { + ctx.onSubmit(data); + }} + onCancel={() => back()} + onDelete={() => { + setOpen(true); + }} + customChild={ + ctx.schema ? ( + ctx.setErrors(_.map(errs, error => error.stack))} + onChange={event => { + setData(event.formData); + }} + /> + ) : ( + <> + ) + } + yamlChild={ + { + const updatedData = safeYAMLToJS(content); + setData(updatedData); + ctx.onSubmit(updatedData); + }} + /> + } + /> + + setOpen(false)} + footer={ +
+ + +
+ } + > + + + {`${t('This action cannot be undone.')} ${ + isFlowCollector + ? t('It will destroy all pods, services and other objects in the namespace') + : t('The following metric will not be collected anymore') + }`} +   + + {ctx.data.spec ? ctx.data.spec[isFlowCollector ? 'namespace' : 'metricName'] : ''} + + . + + +
+
+ ); + }} +
+ ); +}; + +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..7981cbdb7 --- /dev/null +++ b/web/src/components/forms/resource-status.tsx @@ -0,0 +1,76 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +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: any | 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 any[]; + return ( + c.type === 'Ready')?.message} variant={'compact'}> + + + + + + + + + + + {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..f68aad31d --- /dev/null +++ b/web/src/components/forms/resource-watcher.tsx @@ -0,0 +1,204 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { k8sCreate, k8sDelete, k8sUpdate, useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; +import { Bullseye, Spinner } from '@patternfly/react-core'; +import { JSONSchema7 } from 'json-schema'; +import React, { FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useK8sModel } from '../../utils/k8s-models-hook'; +import { back } from '../dynamic-loader/dynamic-loader'; +import { Error as ErrorComponent } from '../messages/error'; +import { prune } from './dynamic-form/utils'; +import './forms.css'; +import { ClusterServiceVersionList, CustomResourceDefinitionKind } from './types'; +import { exampleForModel } from './utils'; + +export type ResourceWatcherProps = { + group: string; + version: string; + kind: string; + name?: string; + namespace?: string; + onSuccess?: (data: any) => void; + children: JSX.Element; + skipErrors?: boolean; + skipCRError?: boolean; + ignoreCSVExample?: boolean; +}; + +export type ResourceWatcherContext = { + group: string; + version: string; + kind: string; + isUpdate: boolean; + schema: JSONSchema7 | null; + data: any; + onSubmit: (data: any, isDelete?: boolean) => void; + loadError: any; + errors: string[]; + setErrors: (errors: string[]) => void; +}; + +export const { Provider, Consumer } = React.createContext({ + group: '', + version: '', + kind: '', + isUpdate: false, + schema: null, + data: {}, + onSubmit: () => { + console.error('onSubmit is not initialized !'); + }, + loadError: null, + errors: [], + setErrors: (errs: string[]) => { + console.error('setErrors is not initialized !', errs); + } +}); + +export const ResourceWatcher: FC = ({ + group, + version, + kind, + name, + namespace, + onSuccess, + children, + skipErrors, + skipCRError, + ignoreCSVExample +}) => { + if (!group || !version || !kind) { + throw new Error('ResourceForm error: apiVersion and kind must be provided'); + } + const { t } = useTranslation('plugin__netobserv-plugin'); + + const [matchingCSVs, csvLoaded, csvLoadError] = useK8sWatchResource({ + groupVersionKind: { + group: 'operators.coreos.com', + version: 'v1alpha1', + kind: 'ClusterServiceVersion' + }, + kind: 'ClusterServiceVersion', + namespace: 'openshift-netobserv-operator' + }); + const [crd, crdLoaded, crdLoadError] = useK8sWatchResource({ + groupVersionKind: { + group: 'apiextensions.k8s.io', + version: 'v1', + kind: 'CustomResourceDefinition' + }, + kind: 'CustomResourceDefinition', + name: kind === 'FlowCollector' ? 'flowcollectors.flows.netobserv.io' : 'flowmetrics.flows.netobserv.io', + isList: false + }); + const [cr, crLoaded, crLoadError] = useK8sWatchResource( + name + ? { + groupVersionKind: { + group, + version, + kind + }, + kind, + name, + namespace, + isList: false + } + : null + ); + + const model = useK8sModel(group, version, kind); + const [errors, setErrors] = React.useState([]); + + if (!skipErrors && (csvLoadError || crdLoadError || (!skipCRError && crLoadError))) { + return ( + + ); + } else if (!csvLoaded || !crdLoaded || (!skipCRError && !crLoaded)) { + return ( + + + + ); + } + + const data = cr + ? { apiVersion: `${group}/${version}`, kind, ...cr } + : !ignoreCSVExample && matchingCSVs?.items?.length + ? exampleForModel( + matchingCSVs.items.find(csv => + csv.spec.customresourcedefinitions?.owned?.some((crd: any) => crd.kind === kind) + ), + group, + version, + kind + ) + : { apiVersion: `${group}/${version}`, kind }; + const schema = crd?.spec?.versions?.find(v => v.name === version)?.schema?.openAPIV3Schema || null; + // force name and namespace to be present in the form when namespaced + if (crd?.spec?.scope === 'Namespaced') { + data.metadata = { + ...data.metadata, + namespace: data.metadata?.namespace || 'netobserv', // for now, keep namespace if exists, or use netobserv by default + // namespace: namespace || 'netobserv', TODO: uncomment alongside with https://issues.redhat.com/browse/NETOBSERV-1690 + name: name + }; + if (schema?.properties?.metadata) { + (schema.properties.metadata as any).properties = { + name: { type: 'string' }, + namespace: { type: 'string' } + }; + (schema.properties.metadata as any).required = ['name', 'namespace']; + } + } + return ( + { + if (isDelete) { + k8sDelete({ + model, + resource: { + apiVersion: data.apiVersion, + kind: data.kind, + metadata: data.metadata + } + }) + .then(() => { + back(); + }) + .catch(e => setErrors([e.message])); + } else { + const apiFunc = cr ? k8sUpdate : k8sCreate; + apiFunc({ + data: prune(data), + model + }) + .then(res => { + setErrors([]); + onSuccess && onSuccess(res); + }) + .catch(e => setErrors([e.message])); + } + } + }} + > + {children} + + ); +}; + +export default ResourceWatcher; diff --git a/web/src/components/forms/types.ts b/web/src/components/forms/types.ts new file mode 100644 index 000000000..d78117d54 --- /dev/null +++ b/web/src/components/forms/types.ts @@ -0,0 +1,161 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk'; +import { JSONSchema7 } from 'json-schema'; + +export enum InstallModeType { + InstallModeTypeOwnNamespace = 'OwnNamespace', + InstallModeTypeSingleNamespace = 'SingleNamespace', + InstallModeTypeMultiNamespace = 'MultiNamespace', + InstallModeTypeAllNamespaces = 'AllNamespaces' +} + +export enum ClusterServiceVersionPhase { + CSVPhaseNone = '', + CSVPhasePending = 'Pending', + CSVPhaseInstallReady = 'InstallReady', + CSVPhaseInstalling = 'Installing', + CSVPhaseSucceeded = 'Succeeded', + CSVPhaseFailed = 'Failed', + CSVPhaseUnknown = 'Unknown', + CSVPhaseReplacing = 'Replacing', + CSVPhaseDeleting = 'Deleting' +} + +export enum CSVConditionReason { + CSVReasonRequirementsUnknown = 'RequirementsUnknown', + CSVReasonRequirementsNotMet = 'RequirementsNotMet', + CSVReasonRequirementsMet = 'AllRequirementsMet', + CSVReasonOwnerConflict = 'OwnerConflict', + CSVReasonComponentFailed = 'InstallComponentFailed', + CSVReasonInvalidStrategy = 'InvalidInstallStrategy', + CSVReasonWaiting = 'InstallWaiting', + CSVReasonInstallSuccessful = 'InstallSucceeded', + CSVReasonInstallCheckFailed = 'InstallCheckFailed', + CSVReasonComponentUnhealthy = 'ComponentUnhealthy', + CSVReasonBeingReplaced = 'BeingReplaced', + CSVReasonReplaced = 'Replaced', + CSVReasonCopied = 'Copied' +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type Descriptor = { + path: string; + displayName: string; + description: string; + 'x-descriptors'?: T[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + value?: any; +}; + +export type CRDDescription = { + name: string; + version: string; + kind: string; + displayName: string; + description?: string; + specDescriptors?: Descriptor[]; + statusDescriptors?: Descriptor[]; + resources?: { + name?: string; + version: string; + kind: string; + }[]; +}; + +export type APIServiceDefinition = { + name: string; + group: string; + version: string; + kind: string; + deploymentName: string; + containerPort: number; + displayName: string; + description?: string; + specDescriptors?: Descriptor[]; + statusDescriptors?: Descriptor[]; + resources?: { + name?: string; + version: string; + kind: string; + }[]; +}; + +export type ClusterServiceVersionIcon = { base64data: string; mediatype: string }; + +export type RequirementStatus = { + group: string; + version: string; + kind: string; + name: string; + status: string; + uuid?: string; +}; + +export type ClusterServiceVersionList = { + apiVersion: 'operators.coreos.com/v1alpha1'; + kind: 'ClusterServiceVersionList'; + items?: ClusterServiceVersionKind[]; +}; + +export type ClusterServiceVersionKind = { + apiVersion: 'operators.coreos.com/v1alpha1'; + kind: 'ClusterServiceVersion'; + spec: { + install: { + strategy: 'Deployment'; + spec?: { + permissions: { + serviceAccountName: string; + rules: { apiGroups: string[]; resources: string[]; verbs: string[] }[]; + }[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + deployments: { name: string; spec: any }[]; + }; + }; + customresourcedefinitions?: { owned?: CRDDescription[]; required?: CRDDescription[] }; + apiservicedefinitions?: { owned?: APIServiceDefinition[]; required?: APIServiceDefinition[] }; + replaces?: string; + installModes?: { type: InstallModeType; supported: boolean }[]; + displayName?: string; + description?: string; + provider?: { name: string }; + version?: string; + icon?: ClusterServiceVersionIcon[]; + }; + status?: { + phase: ClusterServiceVersionPhase; + reason: CSVConditionReason; + message?: string; + requirementStatus?: RequirementStatus[]; + }; +} & any; + +export type CRDVersion = { + name: string; + served: boolean; + storage: boolean; + schema: { + // NOTE: Actually a subset of JSONSchema, but using this type for convenience + openAPIV3Schema: JSONSchema7; + }; +}; + +export type CustomResourceDefinitionKind = { + apiVersion: 'apiextensions.k8s.io/v1'; + kind: 'CustomResourceDefinition'; + spec: { + group: string; + versions: CRDVersion[]; + names: { + kind: string; + singular: string; + plural: string; + listKind: string; + shortNames?: string[]; + }; + scope: 'Cluster' | 'Namespaced'; + }; + status?: { + conditions?: any[]; + }; +} & K8sResourceCommon; diff --git a/web/src/components/forms/utils.ts b/web/src/components/forms/utils.ts new file mode 100644 index 000000000..15925ea13 --- /dev/null +++ b/web/src/components/forms/utils.ts @@ -0,0 +1,100 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { UiSchema } from '@rjsf/utils'; +import _ from 'lodash'; +import { ClusterServiceVersionKind } from './types'; + +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; +}; + +export const exampleForModel = ( + csv: ClusterServiceVersionKind | undefined, + group: string, + version: string, + kind: string +) => + _.defaultsDeep( + {}, + { + kind, + apiVersion: `${group}/${version}` + }, + _.find(parseALMExamples(csv), (s: any) => s.kind === kind && s.apiVersion === `${group}/${version}`) + ) as any; + +export const parseALMExamples = (csv?: ClusterServiceVersionKind) => { + try { + return JSON.parse(csv?.metadata?.annotations?.['alm-examples'] ?? '[]'); + } catch (e) { + // eslint-disable-next-line no-console + console.warn('Unable to parse ALM expamples\n', e); + return []; + } +}; diff --git a/web/src/utils/context.ts b/web/src/utils/context.ts index 404bf275a..c60365918 100644 --- a/web/src/utils/context.ts +++ b/web/src/utils/context.ts @@ -28,6 +28,12 @@ export class ContextSingleton { return ContextSingleton.instance; } + public static setStandalone() { + const instance = ContextSingleton.getInstance(); + instance.isStandalone = true; + instance.host = ''; + } + public static setContext( extensions: ResolvedExtension< ModelFeatureFlag, diff --git a/web/src/utils/k8s-models-hook.ts b/web/src/utils/k8s-models-hook.ts index 8003dd509..35a0e1ddc 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..4681c5ff4 100644 --- a/web/src/utils/url.ts +++ b/web/src/utils/url.ts @@ -2,6 +2,10 @@ import _ from 'lodash'; import { navigate } from '../components/dynamic-loader/dynamic-loader'; export const netflowTrafficPath = '/netflow-traffic'; +export const flowCollectorNewPath = '/k8s/cluster/flows.netobserv.io~v1beta2~FlowCollector/~new'; +export const flowCollectorEditPath = '/k8s/cluster/flows.netobserv.io~v1beta2~FlowCollector/cluster'; +export const flowCollectorStatusPath = '/k8s/cluster/flows.netobserv.io~v1beta2~FlowCollector/status'; +export const flowMetricNewPath = '/k8s/cluster/flows.netobserv.io~v1alpha1~FlowMetric/~new'; // 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 9c2110f87..596220bb7 100644 --- a/web/webpack.config.ts +++ b/web/webpack.config.ts @@ -100,6 +100,10 @@ module.exports = { }, }; +if (process.env.FLAVOR === 'static') { + module.exports.output.path = path.resolve(__dirname, 'dist', 'static'); +} + if (process.env.NODE_ENV === 'production') { module.exports.mode = 'production'; module.exports.output.filename = '[name]-bundle-[hash].min.js';