From 82594f85c878f38346049cf07aabd3c2307336ba Mon Sep 17 00:00:00 2001 From: Shweta Padubidri Date: Thu, 30 May 2024 19:49:59 -0400 Subject: [PATCH] feat: add Korrel8r plugin to UITroubleshootPanel Signed-off-by: Shweta Padubidri --- .../observability.openshift.io_uiplugins.yaml | 11 -- cmd/operator/main.go | 1 + .../observability.openshift.io_uiplugins.yaml | 11 -- docs/api.md | 41 ---- docs/user-guides/observability-ui-plugins.md | 2 +- pkg/apis/uiplugin/v1alpha1/types.go | 19 -- .../v1alpha1/zz_generated.deepcopy.go | 16 -- pkg/controllers/uiplugin/components.go | 186 +++++++++++++++++- pkg/controllers/uiplugin/config/korrel8r.yaml | 20 ++ pkg/controllers/uiplugin/controller.go | 2 +- .../uiplugin/plugin_info_builder.go | 18 +- .../uiplugin/troubleshooting_panel.go | 41 ++-- 12 files changed, 249 insertions(+), 119 deletions(-) create mode 100644 pkg/controllers/uiplugin/config/korrel8r.yaml diff --git a/bundle/manifests/observability.openshift.io_uiplugins.yaml b/bundle/manifests/observability.openshift.io_uiplugins.yaml index 3bbd6147d..da8aae363 100644 --- a/bundle/manifests/observability.openshift.io_uiplugins.yaml +++ b/bundle/manifests/observability.openshift.io_uiplugins.yaml @@ -148,17 +148,6 @@ spec: description: TroubleshootingPanel contains configuration for the troubleshooting console plugin. properties: - korrel8r: - description: korrel8r defines the Korrel8r instance that the troubleshooting - panel plugin will connect to - properties: - name: - description: Name of the korrel8r instance - type: string - namespace: - description: Namespace of the korrel8r instance - type: string - type: object timeout: description: |- Timeout is the maximum duration before a query timeout. diff --git a/cmd/operator/main.go b/cmd/operator/main.go index 1042dde98..165940314 100644 --- a/cmd/operator/main.go +++ b/cmd/operator/main.go @@ -43,6 +43,7 @@ var defaultImages = map[string]string{ "ui-troubleshooting-panel": "quay.io/openshift-observability-ui/troubleshooting-panel-console-plugin:v0.1.0", "ui-distributed-tracing": "quay.io/openshift-observability-ui/distributed-tracing-console-plugin:v0.1.0", "ui-logging": "quay.io/openshift-logging/logging-view-plugin:6.0.0", + "korrel8r": "quay.io/korrel8r/korrel8r:0.6.5", } func imagesUsed() []string { diff --git a/deploy/crds/common/observability.openshift.io_uiplugins.yaml b/deploy/crds/common/observability.openshift.io_uiplugins.yaml index fb36144a4..9d5e315ad 100644 --- a/deploy/crds/common/observability.openshift.io_uiplugins.yaml +++ b/deploy/crds/common/observability.openshift.io_uiplugins.yaml @@ -148,17 +148,6 @@ spec: description: TroubleshootingPanel contains configuration for the troubleshooting console plugin. properties: - korrel8r: - description: korrel8r defines the Korrel8r instance that the troubleshooting - panel plugin will connect to - properties: - name: - description: Name of the korrel8r instance - type: string - namespace: - description: Namespace of the korrel8r instance - type: string - type: object timeout: description: |- Timeout is the maximum duration before a query timeout. diff --git a/docs/api.md b/docs/api.md index 95e0f7ba6..27eff1905 100644 --- a/docs/api.md +++ b/docs/api.md @@ -3124,13 +3124,6 @@ TroubleshootingPanel contains configuration for the troubleshooting console plug - korrel8r - object - - korrel8r defines the Korrel8r instance that the troubleshooting panel plugin will connect to
- - false - timeout string @@ -3145,40 +3138,6 @@ or 'm' (minutes).
-### UIPlugin.spec.troubleshootingPanel.korrel8r -[↩ Parent](#uipluginspectroubleshootingpanel) - - - -korrel8r defines the Korrel8r instance that the troubleshooting panel plugin will connect to - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
namestring - Name of the korrel8r instance
-
false
namespacestring - Namespace of the korrel8r instance
-
false
- - ### UIPlugin.status [↩ Parent](#uiplugin) diff --git a/docs/user-guides/observability-ui-plugins.md b/docs/user-guides/observability-ui-plugins.md index 02719fc40..78edc233a 100644 --- a/docs/user-guides/observability-ui-plugins.md +++ b/docs/user-guides/observability-ui-plugins.md @@ -25,7 +25,7 @@ spec: ### Troubleshooting Panel -The plugin will connect to a Korrel8r instance named `korrel8r` in the `korrel8r` namespace. A "Troubleshooting Panel" button is added to the alerts page, which will convert the current alert into a Korrel8r query, then retrieve related neighbors and display them in a topology view. +The plugin will connect to a Korrel8r service named `korrel8r` in the namespace where the observability operator(and Korrel8r) is deployed. A "Troubleshooting Panel" button is added to the alerts page, which will convert the current alert into a Korrel8r query, then retrieve related neighbors and display them in a topology view. To enable the troubleshooting panel console plugin, create a `UIPlugin` CR. The following example shows how to create a CR to enable the troubleshooting panel console plugin: diff --git a/pkg/apis/uiplugin/v1alpha1/types.go b/pkg/apis/uiplugin/v1alpha1/types.go index 15220d954..9736fdf8f 100644 --- a/pkg/apis/uiplugin/v1alpha1/types.go +++ b/pkg/apis/uiplugin/v1alpha1/types.go @@ -75,25 +75,6 @@ type TroubleshootingPanelConfig struct { // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="OCP Console Query Timeout",xDescriptors={"urn:alm:descriptor:com.tectonic.ui:ocpConsoleTimeout"} // +kubebuilder:validation:Pattern:="^([0-9]+)([sm]{1})$" Timeout string `json:"timeout,omitempty"` - // korrel8r defines the Korrel8r instance that the troubleshooting panel plugin will connect to - // - // +optional - // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Korrel8r Instance" - Korrel8r TroubleshootingPanelKorrel8rConfig `json:"korrel8r,omitempty"` -} - -type TroubleshootingPanelKorrel8rConfig struct { - // Name of the korrel8r instance - // - // +optional - // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Korrel8r Instance Name" - Name string `json:"name,omitempty"` - - // Namespace of the korrel8r instance - // - // +optional - // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Korrel8r Instance Namespace" - Namespace string `json:"namespace,omitempty"` } // DistributedTracingConfig contains options for configuring the Distributed Tracing plugin diff --git a/pkg/apis/uiplugin/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/uiplugin/v1alpha1/zz_generated.deepcopy.go index 9ca85326d..c72a7c3dc 100644 --- a/pkg/apis/uiplugin/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/uiplugin/v1alpha1/zz_generated.deepcopy.go @@ -119,7 +119,6 @@ func (in *LokiStackReference) DeepCopy() *LokiStackReference { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TroubleshootingPanelConfig) DeepCopyInto(out *TroubleshootingPanelConfig) { *out = *in - out.Korrel8r = in.Korrel8r } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TroubleshootingPanelConfig. @@ -132,21 +131,6 @@ func (in *TroubleshootingPanelConfig) DeepCopy() *TroubleshootingPanelConfig { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *TroubleshootingPanelKorrel8rConfig) DeepCopyInto(out *TroubleshootingPanelKorrel8rConfig) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TroubleshootingPanelKorrel8rConfig. -func (in *TroubleshootingPanelKorrel8rConfig) DeepCopy() *TroubleshootingPanelKorrel8rConfig { - if in == nil { - return nil - } - out := new(TroubleshootingPanelKorrel8rConfig) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *UIPlugin) DeepCopyInto(out *UIPlugin) { *out = *in diff --git a/pkg/controllers/uiplugin/components.go b/pkg/controllers/uiplugin/components.go index 1285acf64..8e8d0c75b 100644 --- a/pkg/controllers/uiplugin/components.go +++ b/pkg/controllers/uiplugin/components.go @@ -1,9 +1,13 @@ package uiplugin import ( + "bytes" + "embed" "fmt" "hash/fnv" + "io" "sort" + "text/template" osv1alpha1 "github.com/openshift/api/console/v1alpha1" appsv1 "k8s.io/api/apps/v1" @@ -18,9 +22,13 @@ import ( ) const ( - port = 9443 - serviceAccountSuffix = "-sa" - servingCertVolumeName = "serving-cert" + port = 9443 + serviceAccountSuffix = "-sa" + servingCertVolumeName = "serving-cert" + Korrel8rConfigFileName = "korrel8r.yaml" + Korrel8rConfigMountDir = "/config/" + OpenshiftLoggingNs = "openshift-logging" + OpenshiftNetobservNs = "netobserv" annotationPrefix = "observability.openshift.io/ui-plugin-" ) @@ -31,6 +39,9 @@ var ( } hashSeparator = []byte("\n") + + //go:embed config/korrel8r.yaml + korrel8rConfigYAMLTmplFile embed.FS ) func pluginComponentReconcilers(plugin *uiv1alpha1.UIPlugin, pluginInfo UIPluginInfo) []reconciler.Reconciler { @@ -67,6 +78,16 @@ func pluginComponentReconcilers(plugin *uiv1alpha1.UIPlugin, pluginInfo UIPlugin } } + if pluginInfo.Korrel8rImage != "" { + kname := "korrel8r" + components = append(components, reconciler.NewUpdater(newKorrel8rService(kname, namespace), plugin)) + korrel8rCm, err := newKorrel8rConfigMap(kname, namespace, pluginInfo) + if err == nil && korrel8rCm != nil { + components = append(components, reconciler.NewUpdater(korrel8rCm, plugin)) + components = append(components, reconciler.NewUpdater(newKorrel8rDeployment(kname, namespace, pluginInfo), plugin)) + } + } + return components } @@ -295,6 +316,165 @@ func newService(info UIPluginInfo, namespace string) *corev1.Service { } } +func newKorrel8rDeployment(name string, namespace string, info UIPluginInfo) *appsv1.Deployment { + volumes := []corev1.Volume{ + { + Name: servingCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: name, + DefaultMode: ptr.To(int32(420)), + }, + }, + }, + } + volumeMounts := []corev1.VolumeMount{ + { + Name: servingCertVolumeName, + ReadOnly: true, + MountPath: "/secrets/", + }, + } + + volumes = append(volumes, corev1.Volume{ + Name: "korrel8r-config", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: name, + }, + }, + }, + }) + volumeMounts = append(volumeMounts, corev1.VolumeMount{ + Name: "korrel8r-config", + ReadOnly: true, + MountPath: Korrel8rConfigMountDir, + }) + + deploy := &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + APIVersion: appsv1.SchemeGroupVersion.String(), + Kind: "Deployment", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: componentLabels(name), + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: componentLabels(name), + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: componentLabels(name), + }, + Spec: corev1.PodSpec{ + ServiceAccountName: info.Name + serviceAccountSuffix, + Containers: []corev1.Container{ + { + Name: name, + Image: info.Korrel8rImage, + Command: []string{"korrel8r", "web", "--https=:8443", "--cert=/secrets/tls.crt", "--key=/secrets/tls.key", "--config=/config/korrel8r.yaml"}, + Ports: []corev1.ContainerPort{ + { + ContainerPort: 8443, + Protocol: corev1.ProtocolTCP, + }, + }, + SecurityContext: &corev1.SecurityContext{ + RunAsNonRoot: ptr.To(true), + AllowPrivilegeEscalation: ptr.To(false), + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{ + "ALL", + }, + }, + SeccompProfile: &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeRuntimeDefault, + }, + }, + VolumeMounts: volumeMounts, + }, + }, + Volumes: volumes, + }, + }, + }, + } + return deploy +} + +func newKorrel8rService(name string, namespace string) *corev1.Service { + annotations := map[string]string{ + "service.beta.openshift.io/serving-cert-secret-name": name, + } + + return &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Service", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: componentLabels(name), + Annotations: annotations, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Port: port, + Name: "web", + Protocol: corev1.ProtocolTCP, + TargetPort: intstr.FromInt32(port), + }, + }, + Selector: componentLabels(name), + Type: corev1.ServiceTypeClusterIP, + }, + } +} + +func newKorrel8rConfigMap(name string, namespace string, info UIPluginInfo) (*corev1.ConfigMap, error) { + + korrel8rData := map[string]string{"Metric": "thanos-querier", "MetricAlert": "alertmanager-main", "Log": "logging-loki-gateway-http", "Netflow": "loki-gateway-http", "MonitoringNs": "openshift-monitoring", "LoggingNs": OpenshiftLoggingNs, "NetobservNs": OpenshiftNetobservNs} + + if info.LokiServiceNames[OpenshiftLoggingNs] != "" { + korrel8rData["LoggingNs"] = info.LokiServiceNames[OpenshiftLoggingNs] + } + if info.LokiServiceNames[OpenshiftNetobservNs] != "" { + korrel8rData["NetobservNs"] = info.LokiServiceNames[OpenshiftNetobservNs] + } + + var korrel8rConfigYAMLTmpl = template.Must(template.ParseFS(korrel8rConfigYAMLTmplFile, "config/korrel8r.yaml")) + w := bytes.NewBuffer(nil) + err := korrel8rConfigYAMLTmpl.Execute(w, korrel8rData) + if err != nil { + return nil, err + } + + cfg, _ := io.ReadAll(w) + + return &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: corev1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: componentLabels(name), + }, + Data: map[string]string{ + Korrel8rConfigFileName: string(cfg), + }, + }, nil +} + func componentLabels(pluginName string) map[string]string { return map[string]string{ "app.kubernetes.io/instance": pluginName, diff --git a/pkg/controllers/uiplugin/config/korrel8r.yaml b/pkg/controllers/uiplugin/config/korrel8r.yaml new file mode 100644 index 000000000..d6b971e4d --- /dev/null +++ b/pkg/controllers/uiplugin/config/korrel8r.yaml @@ -0,0 +1,20 @@ +# Default configuration for deploying Korrel8r as a service in an OpenShift cluster. +# Store service URLs assume that stores are installed in their default locations. +stores: + - domain: k8s + - domain: alert + metrics: https://{{ .Metric }}.{{ .MonitoringNs }}.svc:9091 + alertmanager: https://{{ .MetricAlert }}.{{ .MonitoringNs }}.svc:9094 + certificateAuthority: ./run/secrets/kubernetes.io/serviceaccount/service-ca.crt + - domain: log + lokiStack: https://{{ .Log }}.{{ .LoggingNs }}.svc:8080 + certificateAuthority: ./run/secrets/kubernetes.io/serviceaccount/service-ca.crt + - domain: metric + metric: https://{{ .Metric }}.{{ .MonitoringNs }}.svc:9091 + certificateAuthority: ./run/secrets/kubernetes.io/serviceaccount/service-ca.crt + - domain: netflow + lokiStack: https://{{ .Netflow }}.{{ .NetobservNs }}.svc:8080 + certificateAuthority: ./run/secrets/kubernetes.io/serviceaccount/service-ca.crt + +include: + - /etc/korrel8r/rules/all.yaml \ No newline at end of file diff --git a/pkg/controllers/uiplugin/controller.go b/pkg/controllers/uiplugin/controller.go index 99366adac..229cc94d1 100644 --- a/pkg/controllers/uiplugin/controller.go +++ b/pkg/controllers/uiplugin/controller.go @@ -154,7 +154,7 @@ func (rm resourceManager) Reconcile(ctx context.Context, req ctrl.Request) (ctrl return ctrl.Result{}, nil } - pluginInfo, err := PluginInfoBuilder(plugin, rm.pluginConf, rm.clusterVersion) + pluginInfo, err := PluginInfoBuilder(ctx, rm.k8sClient, plugin, rm.pluginConf, rm.clusterVersion) if err != nil { logger.Error(err, "failed to reconcile plugin") diff --git a/pkg/controllers/uiplugin/plugin_info_builder.go b/pkg/controllers/uiplugin/plugin_info_builder.go index 564e6479a..76cfaccc3 100644 --- a/pkg/controllers/uiplugin/plugin_info_builder.go +++ b/pkg/controllers/uiplugin/plugin_info_builder.go @@ -1,18 +1,22 @@ package uiplugin import ( + "context" "fmt" osv1alpha1 "github.com/openshift/api/console/v1alpha1" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" uiv1alpha1 "github.com/rhobs/observability-operator/pkg/apis/uiplugin/v1alpha1" ) type UIPluginInfo struct { Image string + Korrel8rImage string + LokiServiceNames map[string]string Name string ConsoleName string DisplayName string @@ -26,7 +30,7 @@ type UIPluginInfo struct { ResourceNamespace string } -func PluginInfoBuilder(plugin *uiv1alpha1.UIPlugin, pluginConf UIPluginsConfiguration, clusterVersion string) (*UIPluginInfo, error) { +func PluginInfoBuilder(ctx context.Context, k client.Client, plugin *uiv1alpha1.UIPlugin, pluginConf UIPluginsConfiguration, clusterVersion string) (*UIPluginInfo, error) { compatibilityInfo, err := lookupImageAndFeatures(plugin.Spec.Type, clusterVersion) if err != nil { return nil, err @@ -108,7 +112,17 @@ func PluginInfoBuilder(plugin *uiv1alpha1.UIPlugin, pluginConf UIPluginsConfigur return pluginInfo, nil } case uiv1alpha1.TypeTroubleshootingPanel: - return createTroubleshootingPanelPluginInfo(plugin, namespace, plugin.Name, image, []string{}) + { + pluginInfo, err := createTroubleshootingPanelPluginInfo(plugin, namespace, plugin.Name, image, []string{}) + if err == nil { + pluginInfo.Korrel8rImage = pluginConf.Images["korrel8r"] + pluginInfo.LokiServiceNames[OpenshiftLoggingNs], err = getLokiServiceName(ctx, k, OpenshiftLoggingNs) + } + if err == nil { + pluginInfo.LokiServiceNames[OpenshiftNetobservNs], err = getLokiServiceName(ctx, k, OpenshiftNetobservNs) + } + return pluginInfo, err + } case uiv1alpha1.TypeDistributedTracing: return createDistributedTracingPluginInfo(plugin, namespace, plugin.Name, image, []string{}) case uiv1alpha1.TypeLogging: diff --git a/pkg/controllers/uiplugin/troubleshooting_panel.go b/pkg/controllers/uiplugin/troubleshooting_panel.go index 5a27e5504..28931026b 100644 --- a/pkg/controllers/uiplugin/troubleshooting_panel.go +++ b/pkg/controllers/uiplugin/troubleshooting_panel.go @@ -2,6 +2,7 @@ package uiplugin import ( "bytes" + "context" "fmt" "strings" @@ -9,12 +10,14 @@ import ( "gopkg.in/yaml.v3" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" uiv1alpha1 "github.com/rhobs/observability-operator/pkg/apis/uiplugin/v1alpha1" ) func createTroubleshootingPanelPluginInfo(plugin *uiv1alpha1.UIPlugin, namespace, name, image string, features []string) (*UIPluginInfo, error) { troubleshootingPanelConfig := plugin.Spec.TroubleshootingPanel + korrel8rSvcName := "korrel8r" configYaml, err := marshalTroubleshootingPanelPluginConfig(troubleshootingPanelConfig) if err != nil { @@ -29,23 +32,13 @@ func createTroubleshootingPanelPluginInfo(plugin *uiv1alpha1.UIPlugin, namespace extraArgs = append(extraArgs, fmt.Sprintf("-features=%s", strings.Join(features, ","))) } - proxyName, proxyNamespace := "korrel8r", "korrel8r" - - if plugin.Spec.TroubleshootingPanel != nil { - if plugin.Spec.TroubleshootingPanel.Korrel8r.Name != "" { - proxyName = plugin.Spec.TroubleshootingPanel.Korrel8r.Name - } - if plugin.Spec.TroubleshootingPanel.Korrel8r.Namespace != "" { - proxyNamespace = plugin.Spec.TroubleshootingPanel.Korrel8r.Namespace - } - } - pluginInfo := &UIPluginInfo{ Image: image, Name: plugin.Name, ConsoleName: "troubleshooting-panel-console-plugin", DisplayName: "Troubleshooting Panel Console Plugin", ResourceNamespace: namespace, + LokiServiceNames: make(map[string]string), ExtraArgs: extraArgs, Proxies: []osv1alpha1.ConsolePluginProxy{ { @@ -53,9 +46,9 @@ func createTroubleshootingPanelPluginInfo(plugin *uiv1alpha1.UIPlugin, namespace Alias: "korrel8r", Authorize: false, Service: osv1alpha1.ConsolePluginProxyServiceConfig{ - Name: proxyName, - Namespace: proxyNamespace, - Port: 8443, + Name: korrel8rSvcName, + Namespace: namespace, + Port: 9443, }, }, }, @@ -99,3 +92,23 @@ func marshalTroubleshootingPanelPluginConfig(cfg *uiv1alpha1.TroubleshootingPane return buf.String(), nil } + +func getLokiServiceName(ctx context.Context, k client.Client, ns string) (string, error) { + + serviceList := &corev1.ServiceList{} + if err := k.List(ctx, serviceList, client.InNamespace(ns)); err != nil { + return "", err + } + + // Accumulate services that contain "gateway" in their names + var gatewayServices []corev1.Service + for _, service := range serviceList.Items { + if strings.Contains(service.Name, "gateway") { + gatewayServices = append(gatewayServices, service) + } + } + if len(gatewayServices) > 0 { + return gatewayServices[0].Name, nil + } + return "", nil +}