From 9182680c35872b923b991b70c85c1a4a5c4a7f7b Mon Sep 17 00:00:00 2001 From: Saswata Mukherjee Date: Thu, 11 Dec 2025 10:44:33 +0000 Subject: [PATCH 1/2] Add tools for Perses dashboard This commit enables listing Perses dashboards from a cluster, getting a specific one, as well as returning out of the box dashboards that we ship with OpenShift platform. The idea is to for the LLM to first look at Out of the box dashboards, to see if it can answer a user's question using panels from those. And if not search wider for any custom dashboards a user might have. We also allow users (and ourselves!) to specify an LLM-friendly description of PersesDashboard objects using an annotation operator.perses.dev/mcp-help! This would help the LLM accurately filter and select dashboards that match a user's query. Signed-off-by: Saswata Mukherjee --- cmd/obs-mcp/main.go | 21 ++++-- go.mod | 34 +++++++++- go.sum | 90 ++++++++++++++++++++++-- pkg/k8s/perses.go | 124 ++++++++++++++++++++++++++++++++++ pkg/mcp/handlers.go | 106 +++++++++++++++++++++++++++-- pkg/mcp/server.go | 19 ++++-- pkg/mcp/tools.go | 86 +++++++++++++++++++++++ pkg/mcp/tools_test.go | 3 + pkg/perses/dashboard.go | 9 +++ pkg/perses/ootb_dashboards.go | 32 +++++++++ 10 files changed, 504 insertions(+), 20 deletions(-) create mode 100644 pkg/k8s/perses.go create mode 100644 pkg/perses/dashboard.go create mode 100644 pkg/perses/ootb_dashboards.go diff --git a/cmd/obs-mcp/main.go b/cmd/obs-mcp/main.go index c380f39..cd4db0e 100644 --- a/cmd/obs-mcp/main.go +++ b/cmd/obs-mcp/main.go @@ -12,6 +12,7 @@ import ( "github.com/mark3labs/mcp-go/server" "github.com/rhobs/obs-mcp/pkg/k8s" "github.com/rhobs/obs-mcp/pkg/mcp" + "github.com/rhobs/obs-mcp/pkg/perses" "github.com/rhobs/obs-mcp/pkg/prometheus" ) @@ -29,6 +30,8 @@ func main() { var guardrails = flag.String("guardrails", "all", "Guardrails configuration: 'all' (default), 'none', or comma-separated list of guardrails to enable (disallow-explicit-name-label, require-label-matcher, disallow-blanket-regex)") var maxMetricCardinality = flag.Uint64("guardrails.max-metric-cardinality", 20000, "Maximum allowed series count per metric (0 = disabled)") var maxLabelCardinality = flag.Uint64("guardrails.max-label-cardinality", 500, "Maximum allowed label value count for blanket regex (0 = always disallow blanket regex). Only takes effect if disallow-blanket-regex is enabled.") + var ootbDashboards = flag.String("ootb-dashboards", "", "Path to YAML file containing out-of-the-box PersesDashboard definitions") + flag.Parse() // Configure slog with specified log level @@ -55,12 +58,22 @@ func main() { parsedGuardrails.MaxLabelCardinality = *maxLabelCardinality } + // Load out-of-the-box dashboards if specified + ootbDashboardsList, err := perses.LoadOOTBDashboards(*ootbDashboards) + if err != nil { + log.Fatalf("Failed to load OOTB dashboards: %v", err) + } + if len(ootbDashboardsList) > 0 { + slog.Info("Loaded out-of-the-box dashboards", "count", len(ootbDashboardsList)) + } + // Create MCP options opts := mcp.ObsMCPOptions{ - AuthMode: parsedAuthMode, - PromURL: promURL, - Insecure: *insecure, - Guardrails: parsedGuardrails, + AuthMode: parsedAuthMode, + PromURL: promURL, + Insecure: *insecure, + Guardrails: parsedGuardrails, + OOTBDashboards: ootbDashboardsList, } // Create MCP server diff --git a/go.mod b/go.mod index 33fca80..ad7c053 100644 --- a/go.mod +++ b/go.mod @@ -4,26 +4,39 @@ go 1.24.6 require ( github.com/mark3labs/mcp-go v0.39.1 + github.com/perses/perses-operator v0.2.0 github.com/prometheus/client_golang v1.23.2 github.com/prometheus/common v0.67.1 github.com/prometheus/prometheus v0.307.3 + gopkg.in/yaml.v3 v3.0.1 + k8s.io/apimachinery v0.34.1 k8s.io/client-go v0.34.1 + sigs.k8s.io/controller-runtime v0.22.4 ) require ( + cel.dev/expr v0.24.0 // indirect + github.com/PaesslerAG/gval v1.2.4 // indirect + github.com/PaesslerAG/jsonpath v0.1.2-0.20240726212847-3a740cf7976f // indirect + github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/brunoga/deep v1.2.5 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dennwc/varint v1.0.0 // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-jose/go-jose/v4 v4.1.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/cel-go v0.26.0 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect @@ -31,34 +44,51 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/labstack/echo/v4 v4.13.4 // indirect + github.com/labstack/gommon v0.4.2 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/muhlemmer/gu v0.3.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect + github.com/nexucis/lamenv v0.5.2 // indirect + github.com/perses/common v0.27.1-0.20250326140707-96e439b14e0e // indirect + github.com/perses/perses v0.51.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/procfs v0.16.1 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/spf13/pflag v1.0.6 // indirect + github.com/stoewer/go-strcase v1.3.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect + github.com/zitadel/oidc/v3 v3.38.1 // indirect + github.com/zitadel/schema v1.3.1 // indirect go.uber.org/atomic v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.42.0 // indirect + golang.org/x/exp v0.0.0-20250808145144-a408d31f581a // indirect golang.org/x/net v0.44.0 // indirect golang.org/x/oauth2 v0.31.0 // indirect golang.org/x/sys v0.36.0 // indirect golang.org/x/term v0.35.0 // indirect golang.org/x/text v0.29.0 // indirect golang.org/x/time v0.13.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.34.1 // indirect - k8s.io/apimachinery v0.34.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect diff --git a/go.sum b/go.sum index 246fcf8..dce4297 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI= cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= @@ -12,8 +14,16 @@ github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDo github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 h1:XkkQbfMyuH2jTSjQjSoihryI8GINRcs4xp8lNawg0FI= github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= +github.com/PaesslerAG/gval v1.2.2/go.mod h1:XRFLwvmkTEdYziLdaCeCa5ImcGVrfQbeNUbVR+C6xac= +github.com/PaesslerAG/gval v1.2.4 h1:rhX7MpjJlcxYwL2eTTYIOBUyEKZ+A96T9vQySWkVUiU= +github.com/PaesslerAG/gval v1.2.4/go.mod h1:XRFLwvmkTEdYziLdaCeCa5ImcGVrfQbeNUbVR+C6xac= +github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8= +github.com/PaesslerAG/jsonpath v0.1.2-0.20240726212847-3a740cf7976f h1:TxDCeKRCgHea2hUiMOjWwqzWmrIGqSOZYkEPuClXzDo= +github.com/PaesslerAG/jsonpath v0.1.2-0.20240726212847-3a740cf7976f/go.mod h1:zTyVtYhYjcHpfCtqnCMxejgp0pEEwb/xJzhn05NrkJk= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= +github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= +github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/aws/aws-sdk-go-v2 v1.39.2 h1:EJLg8IdbzgeD7xgvZ+I8M1e0fL0ptn/M47lianzth0I= github.com/aws/aws-sdk-go-v2 v1.39.2/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY= github.com/aws/aws-sdk-go-v2/config v1.31.12 h1:pYM1Qgy0dKZLHX2cXslNacbcEFMkDMl+Bcj5ROuS6p8= @@ -46,6 +56,8 @@ github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/brunoga/deep v1.2.5 h1:bigq4eooqbeJXfvTfZBn3AH3B1iW+rtetxVeh0GiLrg= +github.com/brunoga/deep v1.2.5/go.mod h1:GDV6dnXqn80ezsLSZ5Wlv1PdKAWAO4L5PnKYtv2dgaI= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -58,16 +70,24 @@ github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE= github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY= +github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= @@ -82,8 +102,11 @@ github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9v github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI= +github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -117,27 +140,46 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA= +github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mark3labs/mcp-go v0.39.1 h1:2oPxk7aDbQhouakkYyKl2T4hKFU1c6FDaubWyGyVE1k= github.com/mark3labs/mcp-go v0.39.1/go.mod h1:T7tUa2jO6MavG+3P25Oy/jR7iCeJPHImCZHRymCn39g= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= +github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nexucis/lamenv v0.5.2 h1:tK/u3XGhCq9qIoVNcXsK9LZb8fKopm0A5weqSRvHd7M= +github.com/nexucis/lamenv v0.5.2/go.mod h1:HusJm6ltmmT7FMG8A750mOLuME6SHCsr2iFYxp5fFi0= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= -github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= -github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= -github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= -github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= +github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= +github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= +github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +github.com/perses/common v0.27.1-0.20250326140707-96e439b14e0e h1:AormqtWdtHdoQyGO90U1fRoElR0XQHmP0W9oJUsCOZY= +github.com/perses/common v0.27.1-0.20250326140707-96e439b14e0e/go.mod h1:CMTbKu0uWCFKgo4oDVoT8GcMC0bKyDH4cNG3GVfi+rA= +github.com/perses/perses v0.51.0 h1:lLssvsMjxFg2oP+vKX6pz2SFTfrUyso/A2/A/6oFens= +github.com/perses/perses v0.51.0/go.mod h1:DrGiL+itTLl2mwEvNa0wGokELfZTsqOc3TEg+2B0uwY= +github.com/perses/perses-operator v0.2.0 h1:gIhKUWca8ncaxyvOk2USaGfQ32eNcXzjDN97UlQAP0M= +github.com/perses/perses-operator v0.2.0/go.mod h1:91gFy0XicXrWSYSr4ChkMp16GSOkeXjKdkXlfEECw5g= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -159,18 +201,35 @@ github.com/prometheus/prometheus v0.307.3 h1:zGIN3EpiKacbMatcUL2i6wC26eRWXdoXfNP github.com/prometheus/prometheus v0.307.3/go.mod h1:sPbNW+KTS7WmzFIafC3Inzb6oZVaGLnSvwqTdz2jxRQ= github.com/prometheus/sigv4 v0.2.1 h1:hl8D3+QEzU9rRmbKIRwMKRwaFGyLkbPdH5ZerglRHY0= github.com/prometheus/sigv4 v0.2.1/go.mod h1:ySk6TahIlsR2sxADuHy4IBFhwEjRGGsfbbLGhFYFj6Q= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= +github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= @@ -179,6 +238,10 @@ github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zI github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/zitadel/oidc/v3 v3.38.1 h1:VTf1Bv/33UbSwJnIWbfEIdpUGYKfoHetuBNIqVTcjvA= +github.com/zitadel/oidc/v3 v3.38.1/go.mod h1:muukzAasaWmn3vBwEVMglJfuTE0PKCvLJGombPwXIRw= +github.com/zitadel/schema v1.3.1 h1:QT3kwiRIRXXLVAs6gCK/u044WmUVh6IlbLXUsn6yRQU= +github.com/zitadel/schema v1.3.1/go.mod h1:071u7D2LQacy1HAN+YnMd/mx1qVE2isb0Mjeqg46xnU= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= @@ -191,8 +254,14 @@ go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJr go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= @@ -222,6 +291,8 @@ golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= @@ -244,6 +315,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.250.0 h1:qvkwrf/raASj82UegU2RSDGWi/89WkLckn4LuO4lVXM= google.golang.org/api v0.250.0/go.mod h1:Y9Uup8bDLJJtMzJyQnu+rLRJLA0wn+wTtc6vTlOvfXo= +google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 h1:8XJ4pajGwOlasW+L13MnEGA8W4115jJySQtVfS2/IBU= +google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk= google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 h1:V1jCN2HBa8sySkR5vLcCSqJSTMv093Rw9EJefhQGP7M= google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= @@ -257,10 +330,13 @@ gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSP gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= +k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI= +k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc= k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= @@ -271,6 +347,8 @@ k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOP k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A= +sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= diff --git a/pkg/k8s/perses.go b/pkg/k8s/perses.go new file mode 100644 index 0000000..1f74ab0 --- /dev/null +++ b/pkg/k8s/perses.go @@ -0,0 +1,124 @@ +package k8s + +import ( + "context" + "encoding/json" + "fmt" + + persesv1alpha1 "github.com/perses/perses-operator/api/v1alpha1" + "github.com/rhobs/obs-mcp/pkg/perses" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + // PersesMCPHelpAnnotation is the annotation key for MCP help description + PersesMCPHelpAnnotation = "operator.perses.dev/mcp-help" +) + +// GetPersesKubeClient returns a controller-runtime client with Perses types registered +func GetPersesKubeClient() (client.Client, error) { + config, err := GetClientConfig() + if err != nil { + return nil, err + } + + scheme := runtime.NewScheme() + if err := persesv1alpha1.AddToScheme(scheme); err != nil { + return nil, fmt.Errorf("failed to add perses scheme: %w", err) + } + + c, err := client.New(config, client.Options{Scheme: scheme}) + if err != nil { + return nil, fmt.Errorf("failed to create controller-runtime client: %w", err) + } + + return c, nil +} + +// ListPersesDashboards lists all PersesDashboard objects across all namespaces or in a specific namespace. +// Uses types from github.com/perses/perses-operator/api/v1alpha1 +// The labelSelector parameter accepts Kubernetes label selector syntax (e.g., "app=myapp,env=prod"). +func ListPersesDashboards(ctx context.Context, namespace, labelSelector string) ([]perses.PersesDashboardInfo, error) { + c, err := GetPersesKubeClient() + if err != nil { + return nil, fmt.Errorf("failed to get perses client: %w", err) + } + + // Build list options + listOpts := &client.ListOptions{} + if namespace != "" { + listOpts.Namespace = namespace + } + if labelSelector != "" { + selector, err := labels.Parse(labelSelector) + if err != nil { + return nil, fmt.Errorf("invalid label selector: %w", err) + } + listOpts.LabelSelector = selector + } + + var dashboardList persesv1alpha1.PersesDashboardList + if err := c.List(ctx, &dashboardList, listOpts); err != nil { + return nil, fmt.Errorf("failed to list PersesDashboards: %w", err) + } + + dbInfos := make([]perses.PersesDashboardInfo, len(dashboardList.Items)) + for i, db := range dashboardList.Items { + dbInfo := perses.PersesDashboardInfo{ + Name: db.Name, + Namespace: db.Namespace, + Labels: db.GetLabels(), + } + + // Extract MCP help description from annotation if present + if annotations := db.GetAnnotations(); annotations != nil { + if description, ok := annotations[PersesMCPHelpAnnotation]; ok { + dbInfo.Description = description + } + } + + dbInfos[i] = dbInfo + } + + return dbInfos, nil +} + +// GetPersesDashboard retrieves a specific PersesDashboard by name and namespace. +// Returns the dashboard name, namespace, and full spec as a map for JSON serialization. +func GetPersesDashboard(ctx context.Context, namespace, name string) (string, string, map[string]interface{}, error) { + c, err := GetPersesKubeClient() + if err != nil { + return "", "", nil, fmt.Errorf("failed to get perses client: %w", err) + } + + var dashboard persesv1alpha1.PersesDashboard + key := client.ObjectKey{Namespace: namespace, Name: name} + if err := c.Get(ctx, key, &dashboard); err != nil { + return "", "", nil, fmt.Errorf("failed to get PersesDashboard %s/%s: %w", namespace, name, err) + } + + // Convert spec to map[string]interface{} for JSON serialization + specMap, err := specToMap(dashboard.Spec) + if err != nil { + return "", "", nil, fmt.Errorf("failed to convert spec to map: %w", err) + } + + return dashboard.Name, dashboard.Namespace, specMap, nil +} + +// specToMap converts a PersesDashboardSpec to a map[string]interface{} for JSON serialization +func specToMap(spec persesv1alpha1.Dashboard) (map[string]interface{}, error) { + data, err := json.Marshal(spec) + if err != nil { + return nil, err + } + + var result map[string]interface{} + if err := json.Unmarshal(data, &result); err != nil { + return nil, err + } + + return result, nil +} diff --git a/pkg/mcp/handlers.go b/pkg/mcp/handlers.go index 6b72bf2..2c5efa4 100644 --- a/pkg/mcp/handlers.go +++ b/pkg/mcp/handlers.go @@ -7,10 +7,10 @@ import ( "log/slog" "time" - promModel "github.com/prometheus/common/model" - "github.com/mark3labs/mcp-go/mcp" "github.com/prometheus/common/model" + "github.com/rhobs/obs-mcp/pkg/k8s" + "github.com/rhobs/obs-mcp/pkg/perses" "github.com/rhobs/obs-mcp/pkg/prometheus" ) @@ -72,7 +72,7 @@ func ExecuteRangeQueryHandler(opts ObsMCPOptions) func(context.Context, mcp.Call } // Parse step duration - stepDuration, err := promModel.ParseDuration(step) + stepDuration, err := model.ParseDuration(step) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("invalid step format: %s", err.Error())), nil } @@ -103,7 +103,7 @@ func ExecuteRangeQueryHandler(opts ObsMCPOptions) func(context.Context, mcp.Call durationStr = "1h" } - duration, err := promModel.ParseDuration(durationStr) + duration, err := model.ParseDuration(durationStr) if err != nil { return errorResult(fmt.Sprintf("invalid duration format: %s", err.Error())) } @@ -171,3 +171,101 @@ func ExecuteRangeQueryHandler(opts ObsMCPOptions) func(context.Context, mcp.Call return mcp.NewToolResultStructured(output, string(jsonResult)), nil } } + +// ListPersesDashboardsHandler handles listing PersesDashboard CRD objects from the cluster. +func ListPersesDashboardsHandler(opts ObsMCPOptions) func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + slog.Info("ListPersesDashboardsHandler called") + slog.Debug("ListPersesDashboardsHandler params", "params", req.Params) + + // Get optional parameters + namespace := req.GetString("namespace", "") + labelSelector := req.GetString("label_selector", "") + + dashboards, err := k8s.ListPersesDashboards(ctx, namespace, labelSelector) + if err != nil { + return errorResult(fmt.Sprintf("failed to list PersesDashboards: %s", err.Error())) + } + + slog.Info("ListPersesDashboardsHandler executed successfully", "resultLength", len(dashboards)) + slog.Debug("ListPersesDashboardsHandler results", "results", dashboards) + + // Convert to output format + dashboardInfos := make([]perses.PersesDashboardInfo, len(dashboards)) + for i, db := range dashboards { + dashboardInfos[i] = perses.PersesDashboardInfo{ + Name: db.Name, + Namespace: db.Namespace, + Labels: db.Labels, + Description: db.Description, + } + } + + output := ListPersesDashboardsOutput{Dashboards: dashboardInfos} + result, err := json.Marshal(output) + if err != nil { + return errorResult(fmt.Sprintf("failed to marshal dashboards: %s", err.Error())) + } + + return mcp.NewToolResultStructured(output, string(result)), nil + } +} + +// OOTBPersesDashboardsHandler handles returning pre-configured out-of-the-box dashboards. +func OOTBPersesDashboardsHandler(opts ObsMCPOptions) func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + slog.Info("OOTBPersesDashboardsHandler called") + + output := OOTBPersesDashboardsOutput{Dashboards: opts.OOTBDashboards} + + slog.Info("OOTBPersesDashboardsHandler executed successfully", "resultLength", len(opts.OOTBDashboards)) + slog.Debug("OOTBPersesDashboardsHandler results", "results", opts.OOTBDashboards) + + result, err := json.Marshal(output) + if err != nil { + return errorResult(fmt.Sprintf("failed to marshal OOTB dashboards: %s", err.Error())) + } + + return mcp.NewToolResultStructured(output, string(result)), nil + } +} + +// GetPersesDashboardHandler handles getting a specific PersesDashboard by name and namespace. +func GetPersesDashboardHandler(opts ObsMCPOptions) func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + slog.Info("GetPersesDashboardHandler called") + slog.Debug("GetPersesDashboardHandler params", "params", req.Params) + + // Get required parameters + name, err := req.RequireString("name") + if err != nil { + return errorResult("name parameter is required and must be a string") + } + + namespace, err := req.RequireString("namespace") + if err != nil { + return errorResult("namespace parameter is required and must be a string") + } + + dashboardName, dashboardNamespace, spec, err := k8s.GetPersesDashboard(ctx, namespace, name) + if err != nil { + return errorResult(fmt.Sprintf("failed to get PersesDashboard: %s", err.Error())) + } + + slog.Info("GetPersesDashboardHandler executed successfully", "name", dashboardName, "namespace", dashboardNamespace) + slog.Debug("GetPersesDashboardHandler spec", "spec", spec) + + output := GetPersesDashboardOutput{ + Name: dashboardName, + Namespace: dashboardNamespace, + Spec: spec, + } + + result, err := json.Marshal(output) + if err != nil { + return errorResult(fmt.Sprintf("failed to marshal dashboard: %s", err.Error())) + } + + return mcp.NewToolResultStructured(output, string(result)), nil + } +} diff --git a/pkg/mcp/server.go b/pkg/mcp/server.go index 3147c4e..bafe09b 100644 --- a/pkg/mcp/server.go +++ b/pkg/mcp/server.go @@ -12,15 +12,17 @@ import ( "time" "github.com/mark3labs/mcp-go/server" + "github.com/rhobs/obs-mcp/pkg/perses" "github.com/rhobs/obs-mcp/pkg/prometheus" ) // ObsMCPOptions contains configuration options for the MCP server type ObsMCPOptions struct { - AuthMode AuthMode - PromURL string - Insecure bool - Guardrails *prometheus.Guardrails + AuthMode AuthMode + PromURL string + Insecure bool + Guardrails *prometheus.Guardrails + OOTBDashboards []perses.PersesDashboardInfo // Out-of-the-box dashboards loaded from YAML } const ( @@ -50,14 +52,23 @@ func SetupTools(mcpServer *server.MCPServer, opts ObsMCPOptions) error { // Create tool definitions listMetricsTool := CreateListMetricsTool() executeRangeQueryTool := CreateExecuteRangeQueryTool() + listPersesDashboardsTool := CreateListPersesDashboardsTool() + ootbPersesDashboardsTool := CreateOOTBPersesDashboardsTool() + getPersesDashboardTool := CreateGetPersesDashboardTool() // Create handlers listMetricsHandler := ListMetricsHandler(opts) executeRangeQueryHandler := ExecuteRangeQueryHandler(opts) + listPersesDashboardsHandler := ListPersesDashboardsHandler(opts) + ootbPersesDashboardsHandler := OOTBPersesDashboardsHandler(opts) + getPersesDashboardHandler := GetPersesDashboardHandler(opts) // Add tools to server mcpServer.AddTool(listMetricsTool, listMetricsHandler) mcpServer.AddTool(executeRangeQueryTool, executeRangeQueryHandler) + mcpServer.AddTool(ootbPersesDashboardsTool, ootbPersesDashboardsHandler) + mcpServer.AddTool(listPersesDashboardsTool, listPersesDashboardsHandler) + mcpServer.AddTool(getPersesDashboardTool, getPersesDashboardHandler) return nil } diff --git a/pkg/mcp/tools.go b/pkg/mcp/tools.go index 3f93ef7..2f7fd2d 100644 --- a/pkg/mcp/tools.go +++ b/pkg/mcp/tools.go @@ -2,6 +2,7 @@ package mcp import ( "github.com/mark3labs/mcp-go/mcp" + "github.com/rhobs/obs-mcp/pkg/perses" ) // ListMetricsOutput defines the output schema for the list_metrics tool. @@ -66,3 +67,88 @@ For historical data queries, use explicit 'start' and 'end' times. mcp.WithOutputSchema[RangeQueryOutput](), ) } + +// ListPersesDashboardsOutput defines the output schema for the list_perses_dashboards tool. +type ListPersesDashboardsOutput struct { + Dashboards []perses.PersesDashboardInfo `json:"dashboards" jsonschema:"description=List of PersesDashboard objects from the cluster"` +} + +func CreateListPersesDashboardsTool() mcp.Tool { + return mcp.NewTool("list_perses_dashboards", + mcp.WithDescription(`List all PersesDashboard custom resources from the Kubernetes cluster. + +PersesDashboard is a Custom Resource from the Perses operator (https://github.com/perses/perses-operator) that defines +dashboard configurations. This tool returns summary information about all available dashboards in the form of a list of names, namespaces, labels, and descriptions. + +IMPORTANT: Before using this tool, first check out_of_the_box_perses_dashboards for curated platform dashboards that are more likely to answer common user questions. +Only use this tool if the out-of-the-box dashboards don't have what the user is looking for. + +You can optionally filter by namespace and/or labels. + +Once you have found the dashboard you need, you can use the get_perses_dashboard tool to get the dashboard's panels and configuration. + +IMPORTANT: If you are looking for a specific dashboard, use this tool first to see if it exists. If it does, use the get_perses_dashboard tool to get the dashboard's panels and configuration. +`), + mcp.WithString("namespace", + mcp.Description("Optional namespace to filter dashboards. Leave empty to list from all namespaces."), + ), + mcp.WithString("label_selector", + mcp.Description("Optional Kubernetes label selector to filter dashboards (e.g., 'app=myapp', 'env=prod,team=platform', 'app in (foo,bar)'). Leave empty to list all dashboards."), + ), + mcp.WithOutputSchema[ListPersesDashboardsOutput](), + ) +} + +// OOTBPersesDashboardsOutput defines the output schema for the out_of_the_box_perses_dashboards tool. +type OOTBPersesDashboardsOutput struct { + Dashboards []perses.PersesDashboardInfo `json:"dashboards" jsonschema:"description=List of curated out-of-the-box PersesDashboard definitions"` +} + +// GetPersesDashboardOutput defines the output schema for the get_perses_dashboard tool. +type GetPersesDashboardOutput struct { + Name string `json:"name" jsonschema:"description=Name of the PersesDashboard"` + Namespace string `json:"namespace" jsonschema:"description=Namespace where the PersesDashboard is located"` + Spec map[string]interface{} `json:"spec" jsonschema:"description=The full dashboard specification including panels, layouts, variables, and datasources"` +} + +func CreateOOTBPersesDashboardsTool() mcp.Tool { + tool := mcp.NewTool("out_of_the_box_perses_dashboards", + mcp.WithDescription(`List curated out-of-the-box PersesDashboard definitions for the platform. + +IMPORTANT: Use this tool FIRST when looking for dashboards. These are pre-configured, curated dashboards that cover common platform observability needs and are +most likely to answer user questions about the platform. + +Only fall back to list_perses_dashboards if the dashboards returned here don't have +what the user is looking for. + +Returns a list of dashboard summaries with name, namespace, labels, and description explaining what each dashboard contains. +`), + mcp.WithOutputSchema[OOTBPersesDashboardsOutput](), + ) + // workaround for tool with no parameter + tool.InputSchema = mcp.ToolInputSchema{} + tool.RawInputSchema = []byte(`{"type":"object","properties":{}}`) + return tool +} + +func CreateGetPersesDashboardTool() mcp.Tool { + return mcp.NewTool("get_perses_dashboard", + mcp.WithDescription(`Get a specific PersesDashboard by name and namespace. This tool is used to get the dashboard's panels and configuration. + +Use the list_perses_dashboards or out_of_the_box_perses_dashboards tool first to find available dashboards, then use this tool to get the full specification of a specific dashboard. + +Returns the dashboard's full specification including panels, layouts, variables, and datasources in JSON format. + +You can glean PromQL queries from the dashboard's panels and variables, as well as production context to allow you to answer a user's questions better. +`), + mcp.WithString("name", + mcp.Required(), + mcp.Description("Name of the PersesDashboard"), + ), + mcp.WithString("namespace", + mcp.Required(), + mcp.Description("Namespace of the PersesDashboard"), + ), + mcp.WithOutputSchema[GetPersesDashboardOutput](), + ) +} diff --git a/pkg/mcp/tools_test.go b/pkg/mcp/tools_test.go index d957c88..e0bd27e 100644 --- a/pkg/mcp/tools_test.go +++ b/pkg/mcp/tools_test.go @@ -314,6 +314,9 @@ func TestToolsHaveOutputSchema(t *testing.T) { tools := []mcp.Tool{ CreateListMetricsTool(), CreateExecuteRangeQueryTool(), + CreateListPersesDashboardsTool(), + CreateOOTBPersesDashboardsTool(), + CreateGetPersesDashboardTool(), } if len(tools) == 0 { diff --git a/pkg/perses/dashboard.go b/pkg/perses/dashboard.go new file mode 100644 index 0000000..8760737 --- /dev/null +++ b/pkg/perses/dashboard.go @@ -0,0 +1,9 @@ +package perses + +// PersesDashboardInfo contains summary information about a PersesDashboard. +type PersesDashboardInfo struct { + Name string `json:"name" jsonschema:"description=Name of the PersesDashboard"` + Namespace string `json:"namespace" jsonschema:"description=Namespace where the PersesDashboard is located"` + Labels map[string]string `json:"labels,omitempty" jsonschema:"description=Labels attached to the PersesDashboard"` + Description string `json:"description,omitempty" jsonschema:"description=Human-readable description of the dashboard and what information it contains (from operator.perses.dev/mcp-help annotation)"` +} diff --git a/pkg/perses/ootb_dashboards.go b/pkg/perses/ootb_dashboards.go new file mode 100644 index 0000000..432f071 --- /dev/null +++ b/pkg/perses/ootb_dashboards.go @@ -0,0 +1,32 @@ +package perses + +import ( + "fmt" + "os" + + "gopkg.in/yaml.v3" +) + +// OOTBDashboardsConfig represents the YAML structure for out-of-the-box dashboards +type OOTBDashboardsConfig struct { + Dashboards []PersesDashboardInfo `yaml:"dashboards"` +} + +// LoadOOTBDashboards loads out-of-the-box dashboard definitions from a YAML file +func LoadOOTBDashboards(filePath string) ([]PersesDashboardInfo, error) { + if filePath == "" { + return nil, nil + } + + data, err := os.ReadFile(filePath) + if err != nil { + return nil, fmt.Errorf("failed to read OOTB dashboards file: %w", err) + } + + var config OOTBDashboardsConfig + if err := yaml.Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("failed to parse OOTB dashboards YAML: %w", err) + } + + return config.Dashboards, nil +} From 3a07d7597ae1ef201811394d829cc929171c5f20 Mon Sep 17 00:00:00 2001 From: Saswata Mukherjee Date: Thu, 11 Dec 2025 13:22:24 +0000 Subject: [PATCH 2/2] Translate list call in handler Signed-off-by: Saswata Mukherjee --- pkg/k8s/perses.go | 23 ++--------------------- pkg/mcp/handlers.go | 18 +++++++++++++----- 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/pkg/k8s/perses.go b/pkg/k8s/perses.go index 1f74ab0..169f59f 100644 --- a/pkg/k8s/perses.go +++ b/pkg/k8s/perses.go @@ -6,7 +6,6 @@ import ( "fmt" persesv1alpha1 "github.com/perses/perses-operator/api/v1alpha1" - "github.com/rhobs/obs-mcp/pkg/perses" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -40,7 +39,7 @@ func GetPersesKubeClient() (client.Client, error) { // ListPersesDashboards lists all PersesDashboard objects across all namespaces or in a specific namespace. // Uses types from github.com/perses/perses-operator/api/v1alpha1 // The labelSelector parameter accepts Kubernetes label selector syntax (e.g., "app=myapp,env=prod"). -func ListPersesDashboards(ctx context.Context, namespace, labelSelector string) ([]perses.PersesDashboardInfo, error) { +func ListPersesDashboards(ctx context.Context, namespace, labelSelector string) ([]persesv1alpha1.PersesDashboard, error) { c, err := GetPersesKubeClient() if err != nil { return nil, fmt.Errorf("failed to get perses client: %w", err) @@ -64,25 +63,7 @@ func ListPersesDashboards(ctx context.Context, namespace, labelSelector string) return nil, fmt.Errorf("failed to list PersesDashboards: %w", err) } - dbInfos := make([]perses.PersesDashboardInfo, len(dashboardList.Items)) - for i, db := range dashboardList.Items { - dbInfo := perses.PersesDashboardInfo{ - Name: db.Name, - Namespace: db.Namespace, - Labels: db.GetLabels(), - } - - // Extract MCP help description from annotation if present - if annotations := db.GetAnnotations(); annotations != nil { - if description, ok := annotations[PersesMCPHelpAnnotation]; ok { - dbInfo.Description = description - } - } - - dbInfos[i] = dbInfo - } - - return dbInfos, nil + return dashboardList.Items, nil } // GetPersesDashboard retrieves a specific PersesDashboard by name and namespace. diff --git a/pkg/mcp/handlers.go b/pkg/mcp/handlers.go index 2c5efa4..d543c01 100644 --- a/pkg/mcp/handlers.go +++ b/pkg/mcp/handlers.go @@ -193,12 +193,20 @@ func ListPersesDashboardsHandler(opts ObsMCPOptions) func(context.Context, mcp.C // Convert to output format dashboardInfos := make([]perses.PersesDashboardInfo, len(dashboards)) for i, db := range dashboards { - dashboardInfos[i] = perses.PersesDashboardInfo{ - Name: db.Name, - Namespace: db.Namespace, - Labels: db.Labels, - Description: db.Description, + dashboardInfo := perses.PersesDashboardInfo{ + Name: db.Name, + Namespace: db.Namespace, + Labels: db.GetLabels(), } + + // Extract MCP help description from annotation if present + if annotations := db.GetAnnotations(); annotations != nil { + if description, ok := annotations[k8s.PersesMCPHelpAnnotation]; ok { + dashboardInfo.Description = description + } + } + + dashboardInfos[i] = dashboardInfo } output := ListPersesDashboardsOutput{Dashboards: dashboardInfos}