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..169f59f --- /dev/null +++ b/pkg/k8s/perses.go @@ -0,0 +1,105 @@ +package k8s + +import ( + "context" + "encoding/json" + "fmt" + + persesv1alpha1 "github.com/perses/perses-operator/api/v1alpha1" + "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) ([]persesv1alpha1.PersesDashboard, 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) + } + + return dashboardList.Items, 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..d543c01 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,109 @@ 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 { + 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} + 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 +}