diff --git a/go.mod b/go.mod index 97fd531b8..4d11ba248 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/google/uuid v1.6.0 github.com/miekg/dns v1.1.72 github.com/modelcontextprotocol/go-sdk v1.6.1 + github.com/ovn-kubernetes/ovn-kubernetes-mcp v0.0.0-20260521143347-5fe0e0972cd5 github.com/prometheus/client_golang v1.23.2 github.com/rhobs/obs-mcp v0.2.0 github.com/spf13/afero v1.15.0 @@ -82,7 +83,7 @@ require ( github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect github.com/fatih/color v1.18.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect - github.com/go-errors/errors v1.4.2 // indirect + github.com/go-errors/errors v1.5.1 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/analysis v0.25.0 // indirect diff --git a/go.sum b/go.sum index 8f3fcddf6..e6bac409b 100644 --- a/go.sum +++ b/go.sum @@ -150,8 +150,8 @@ github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx5 github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= 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-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= -github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= +github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= @@ -238,8 +238,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0= github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= -github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc h1:VBbFa1lDYWEeV5FZKUiYKYT0VxCp9twUmmaq9eb8sXw= -github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= +github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg= +github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -336,14 +336,16 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 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.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI= -github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= -github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= -github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= +github.com/onsi/ginkgo/v2 v2.28.3 h1:4JvMdwtFU0imd8fHx25OJXoDMRexnf8v5NHKYSTTji4= +github.com/onsi/ginkgo/v2 v2.28.3/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= +github.com/onsi/gomega v1.40.0 h1:Vtol0e1MghCD2ZVIilPDIg44XSL9l2QAn8ZNaljWcJc= +github.com/onsi/gomega v1.40.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/ovn-kubernetes/ovn-kubernetes-mcp v0.0.0-20260521143347-5fe0e0972cd5 h1:LKCR4P9D2wR2D1wJlhv887at5/odYJ/Ohk7IoXtTWkk= +github.com/ovn-kubernetes/ovn-kubernetes-mcp v0.0.0-20260521143347-5fe0e0972cd5/go.mod h1:R/Q8gRkYAIjOqrKe576AHveESEmhDIxBCuSfiY7rIB0= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= diff --git a/pkg/mcp/modules.go b/pkg/mcp/modules.go index 2e1a97ca8..fa75cc42c 100644 --- a/pkg/mcp/modules.go +++ b/pkg/mcp/modules.go @@ -9,6 +9,7 @@ import ( _ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kubevirt" _ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/netedge" _ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/openshift" + _ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/ovnkubernetes" _ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/tekton" _ "github.com/rhobs/obs-mcp/pkg/toolset" ) diff --git a/pkg/mcp/testdata/toolsets-ovn-kubernetes-tools.json b/pkg/mcp/testdata/toolsets-ovn-kubernetes-tools.json new file mode 100644 index 000000000..a6287f5ce --- /dev/null +++ b/pkg/mcp/testdata/toolsets-ovn-kubernetes-tools.json @@ -0,0 +1,236 @@ +[ + { + "annotations": { + "idempotentHint": true, + "openWorldHint": true, + "readOnlyHint": true, + "title": "OVN: Get" + }, + "description": "Query records from an OVN database table with flexible filtering.\n\nCan list all records in a table (when no record specified) or get a specific record (when record specified).\n\nCommon Northbound tables: Logical_Switch, Logical_Router, Logical_Switch_Port, Logical_Router_Port, ACL, Address_Set, Port_Group, Load_Balancer, NAT.\nCommon Southbound tables: Chassis, Port_Binding, Datapath_Binding, Logical_Flow, MAC_Binding, Multicast_Group, SB_Global.", + "inputSchema": { + "properties": { + "apply_tail_first": { + "description": "If both head and tail are set and apply_tail_first is true, apply tail before head (default: false)", + "type": "boolean" + }, + "columns": { + "description": "Comma-separated list of columns to display (e.g., \"name,_uuid,ports\")", + "type": "string" + }, + "database": { + "description": "OVN database to query - \"nbdb\" for Northbound or \"sbdb\" for Southbound", + "enum": [ + "nbdb", + "sbdb" + ], + "type": "string" + }, + "head": { + "description": "Return only first N lines (default: 100 if tail is not specified)", + "minimum": 1, + "type": "integer" + }, + "name": { + "description": "Name of the pod running OVN", + "type": "string" + }, + "namespace": { + "description": "Kubernetes namespace of the OVN pod", + "type": "string" + }, + "pattern": { + "description": "Regex pattern to filter results (only applies when listing all records)", + "type": "string" + }, + "record": { + "description": "Record identifier (UUID or name). If not specified, lists all records", + "type": "string" + }, + "table": { + "description": "Name of the OVN table (e.g., \"Logical_Switch\", \"Port_Binding\")", + "type": "string" + }, + "tail": { + "description": "Return only last N lines", + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "namespace", + "name", + "database", + "table" + ], + "type": "object" + }, + "name": "ovn_get", + "title": "OVN: Get" + }, + { + "annotations": { + "idempotentHint": true, + "openWorldHint": true, + "readOnlyHint": true, + "title": "OVN: Logical Flow List" + }, + "description": "List logical flows from the OVN Southbound database.\n\nRuns 'ovn-sbctl lflow-list' to retrieve logical flows which represent the compiled logical network pipeline. Essential for debugging packet forwarding.", + "inputSchema": { + "properties": { + "apply_tail_first": { + "description": "If both head and tail are set and apply_tail_first is true, apply tail before head (default: false)", + "type": "boolean" + }, + "datapath": { + "description": "Datapath name or UUID to filter flows for a specific logical switch/router", + "type": "string" + }, + "head": { + "description": "Return only first N lines (default: 100 if tail is not specified)", + "minimum": 1, + "type": "integer" + }, + "name": { + "description": "Name of the pod running OVN", + "type": "string" + }, + "namespace": { + "description": "Kubernetes namespace of the OVN pod", + "type": "string" + }, + "pattern": { + "description": "Regex pattern to filter flows", + "type": "string" + }, + "tail": { + "description": "Return only last N lines", + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "namespace", + "name" + ], + "type": "object" + }, + "name": "ovn_lflow_list", + "title": "OVN: Logical Flow List" + }, + { + "annotations": { + "idempotentHint": true, + "openWorldHint": true, + "readOnlyHint": true, + "title": "OVN: Show" + }, + "description": "Display a comprehensive overview of OVN configuration from either the Northbound or Southbound database.\n\nFor Northbound (nbdb): Runs 'ovn-nbctl show' and displays logical switches, logical routers, their ports, and connections between them.\nFor Southbound (sbdb): Runs 'ovn-sbctl show' and displays chassis information, port bindings, and their relationships.", + "inputSchema": { + "properties": { + "apply_tail_first": { + "description": "If both head and tail are set and apply_tail_first is true, apply tail before head (default: false)", + "type": "boolean" + }, + "database": { + "description": "OVN database to query - \"nbdb\" for Northbound or \"sbdb\" for Southbound", + "enum": [ + "nbdb", + "sbdb" + ], + "type": "string" + }, + "head": { + "description": "Return only first N lines (default: 100 if tail is not specified)", + "minimum": 1, + "type": "integer" + }, + "name": { + "description": "Name of the pod running OVN (e.g., \"ovnkube-node-xxxxx\")", + "type": "string" + }, + "namespace": { + "description": "Kubernetes namespace of the OVN pod (e.g., \"openshift-ovn-kubernetes\")", + "type": "string" + }, + "tail": { + "description": "Return only last N lines", + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "namespace", + "name", + "database" + ], + "type": "object" + }, + "name": "ovn_show", + "title": "OVN: Show" + }, + { + "annotations": { + "idempotentHint": true, + "openWorldHint": true, + "readOnlyHint": true, + "title": "OVN: Trace" + }, + "description": "Trace a packet through the OVN logical network.\n\nRuns 'ovn-trace' to simulate packet processing through the logical network pipeline. Shows which logical flows match, what actions are taken, and the final disposition. Essential for debugging connectivity issues.\n\nMicroflow examples:\n- inport==\"pod1\" \u0026\u0026 eth.src==00:00:00:00:00:01 \u0026\u0026 ip4.src==10.244.0.5 \u0026\u0026 ip4.dst==10.244.1.5\n- inport==\"pod1\" \u0026\u0026 eth.src==00:00:00:00:00:01 \u0026\u0026 icmp \u0026\u0026 ip4.src==10.244.0.5 \u0026\u0026 ip4.dst==8.8.8.8", + "inputSchema": { + "properties": { + "apply_tail_first": { + "description": "If both head and tail are set and apply_tail_first is true, apply tail before head (default: false)", + "type": "boolean" + }, + "datapath": { + "description": "Name of the logical switch or router to start the trace", + "type": "string" + }, + "head": { + "description": "Return only first N lines (default: 100 if tail is not specified)", + "minimum": 1, + "type": "integer" + }, + "microflow": { + "description": "Microflow specification describing the packet to trace", + "type": "string" + }, + "mode": { + "description": "Output verbosity mode (default: \"detailed\")", + "enum": [ + "detailed", + "summary", + "minimal" + ], + "type": "string" + }, + "name": { + "description": "Name of the pod running OVN", + "type": "string" + }, + "namespace": { + "description": "Kubernetes namespace of the OVN pod", + "type": "string" + }, + "pattern": { + "description": "Regex pattern to filter trace output", + "type": "string" + }, + "tail": { + "description": "Return only last N lines", + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "namespace", + "name", + "datapath", + "microflow" + ], + "type": "object" + }, + "name": "ovn_trace", + "title": "OVN: Trace" + } +] diff --git a/pkg/mcp/toolsets_test.go b/pkg/mcp/toolsets_test.go index c8ca81dea..b1904da39 100644 --- a/pkg/mcp/toolsets_test.go +++ b/pkg/mcp/toolsets_test.go @@ -20,6 +20,7 @@ import ( "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kiali" "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kubevirt" "github.com/containers/kubernetes-mcp-server/pkg/toolsets/openshift" + "github.com/containers/kubernetes-mcp-server/pkg/toolsets/ovnkubernetes" "github.com/containers/kubernetes-mcp-server/pkg/toolsets/tekton" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/stretchr/testify/suite" @@ -178,6 +179,7 @@ func (s *ToolsetsSuite) TestGranularToolsetsTools() { &helm.Toolset{}, &kiali.Toolset{}, &kubevirt.Toolset{}, + &ovnkubernetes.Toolset{}, &tekton.Toolset{}, } for _, testCase := range testCases { diff --git a/pkg/toolsets/ovnkubernetes/ovn.go b/pkg/toolsets/ovnkubernetes/ovn.go new file mode 100644 index 000000000..9ff5ae10e --- /dev/null +++ b/pkg/toolsets/ovnkubernetes/ovn.go @@ -0,0 +1,87 @@ +package ovnkubernetes + +import ( + "context" + + "github.com/google/jsonschema-go/jsonschema" + gosdk "github.com/modelcontextprotocol/go-sdk/mcp" + ovnmcp "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovn/mcp" + "k8s.io/utils/ptr" + + "github.com/containers/kubernetes-mcp-server/pkg/api" + "github.com/containers/kubernetes-mcp-server/pkg/kubernetes" +) + +// newOVNServer creates an ovnmcp.MCPServer that executes OVN CLI commands +// inside the given container of an OVN pod via core.PodsExec. +func newOVNServer(core *kubernetes.Core, container string) (*ovnmcp.MCPServer, error) { + return ovnmcp.NewMCPServer(func(ctx context.Context, namespace, name, _ string, command []string) (string, string, error) { + return core.PodsExec(ctx, namespace, name, container, command) + }) +} + +// initOVNTools returns all upstream OVN tool registrations adapted to the +// project's api.ServerTool format. +func initOVNTools() []api.ServerTool { + regs := ovnmcp.AllToolRegistrations() + tools := make([]api.ServerTool, len(regs)) + for i, reg := range regs { + tools[i] = api.ServerTool{ + Tool: mcpToolToAPITool(reg.Tool), + Handler: makeOVNHandler(reg), + } + } + return tools +} + +// mcpToolToAPITool converts a go-sdk Tool into the project's api.Tool. +func mcpToolToAPITool(t *gosdk.Tool) api.Tool { + schema, _ := t.InputSchema.(*jsonschema.Schema) + tool := api.Tool{ + Name: t.Name, + Description: t.Description, + InputSchema: schema, + } + if t.Annotations != nil { + tool.Annotations.ReadOnlyHint = ptr.To(t.Annotations.ReadOnlyHint) + tool.Annotations.IdempotentHint = ptr.To(t.Annotations.IdempotentHint) + tool.Annotations.OpenWorldHint = t.Annotations.OpenWorldHint + tool.Annotations.DestructiveHint = t.Annotations.DestructiveHint + if t.Annotations.Title != "" { + tool.Annotations.Title = t.Annotations.Title + } + } + // Top-level Title takes precedence per MCP spec: + // "Display name precedence order is: title, annotations.title, then name." + if t.Title != "" { + tool.Annotations.Title = t.Title + } + return tool +} + +// makeOVNHandler returns a tool handler that executes OVN commands in the +// appropriate container for each call. +// +// A fresh MCPServer is created per invocation because the target container +// (nbdb, sbdb, or northd) may differ between calls. +func makeOVNHandler(reg ovnmcp.ToolRegistration) api.ToolHandlerFunc { + return func(params api.ToolHandlerParams) (*api.ToolCallResult, error) { + args := params.GetArguments() + target := reg.TargetSelector(args) + container := containerForTarget(target) + server, err := newOVNServer(kubernetes.NewCore(params), container) + if err != nil { + return api.NewToolCallResult("", err), nil + } + result, err := reg.Execute(server, params.Context, args) + if err != nil { + return api.NewToolCallResult("", err), nil + } + return api.NewToolCallResultStructured(result, nil), nil + } +} + +// containerForTarget maps an upstream execution target to a container name. +func containerForTarget(t ovnmcp.ExecTarget) string { + return string(t) +} diff --git a/pkg/toolsets/ovnkubernetes/toolset.go b/pkg/toolsets/ovnkubernetes/toolset.go new file mode 100644 index 000000000..80623b95e --- /dev/null +++ b/pkg/toolsets/ovnkubernetes/toolset.go @@ -0,0 +1,38 @@ +package ovnkubernetes + +import ( + "github.com/containers/kubernetes-mcp-server/pkg/api" + "github.com/containers/kubernetes-mcp-server/pkg/toolsets" +) + +type Toolset struct{} + +var _ api.Toolset = (*Toolset)(nil) + +func (t *Toolset) GetName() string { + return "ovn-kubernetes" +} + +func (t *Toolset) GetDescription() string { + return "OVN-Kubernetes CNI network troubleshooting tools" +} + +func (t *Toolset) GetTools(_ api.Openshift) []api.ServerTool { + return initOVNTools() +} + +func (t *Toolset) GetPrompts() []api.ServerPrompt { + return nil +} + +func (t *Toolset) GetResources() []api.ServerResource { + return nil +} + +func (t *Toolset) GetResourceTemplates() []api.ServerResourceTemplate { + return nil +} + +func init() { + toolsets.Register(&Toolset{}) +} diff --git a/vendor/github.com/go-errors/errors/.travis.yml b/vendor/github.com/go-errors/errors/.travis.yml index 77a6bccf7..1dc296026 100644 --- a/vendor/github.com/go-errors/errors/.travis.yml +++ b/vendor/github.com/go-errors/errors/.travis.yml @@ -2,7 +2,6 @@ language: go go: - "1.8.x" - - "1.10.x" - - "1.13.x" - - "1.14.x" + - "1.11.x" - "1.16.x" + - "1.21.x" diff --git a/vendor/github.com/go-errors/errors/README.md b/vendor/github.com/go-errors/errors/README.md index 3d7852594..558bc883e 100644 --- a/vendor/github.com/go-errors/errors/README.md +++ b/vendor/github.com/go-errors/errors/README.md @@ -9,7 +9,7 @@ This is particularly useful when you want to understand the state of execution when an error was returned unexpectedly. It provides the type \*Error which implements the standard golang error -interface, so you can use this library interchangably with code that is +interface, so you can use this library interchangeably with code that is expecting a normal error return. Usage @@ -80,3 +80,5 @@ This package is licensed under the MIT license, see LICENSE.MIT for details. * v1.4.0 *BREAKING* v1.4.0 reverted all changes from v1.3.0 and is identical to v1.2.0 * v1.4.1 no code change, but now without an unnecessary cover.out file. * v1.4.2 performance improvement to ErrorStack() to avoid unnecessary work https://github.com/go-errors/errors/pull/40 +* v1.5.0 add errors.Join() and errors.Unwrap() copying the stdlib https://github.com/go-errors/errors/pull/40 +* v1.5.1 fix build on go1.13..go1.19 (broken by adding Join and Unwrap with wrong build constraints) diff --git a/vendor/github.com/go-errors/errors/error_1_13.go b/vendor/github.com/go-errors/errors/error_1_13.go index 0af2fc806..34ab3e00e 100644 --- a/vendor/github.com/go-errors/errors/error_1_13.go +++ b/vendor/github.com/go-errors/errors/error_1_13.go @@ -1,3 +1,4 @@ +//go:build go1.13 // +build go1.13 package errors @@ -6,14 +7,17 @@ import ( baseErrors "errors" ) -// find error in any wrapped error +// As finds the first error in err's tree that matches target, and if one is found, sets +// target to that error value and returns true. Otherwise, it returns false. +// +// For more information see stdlib errors.As. func As(err error, target interface{}) bool { return baseErrors.As(err, target) } // Is detects whether the error is equal to a given error. Errors // are considered equal by this function if they are matched by errors.Is -// or if their contained errors are matched through errors.Is +// or if their contained errors are matched through errors.Is. func Is(e error, original error) bool { if baseErrors.Is(e, original) { return true diff --git a/vendor/github.com/go-errors/errors/error_backward.go b/vendor/github.com/go-errors/errors/error_backward.go index 80b0695e7..ff14c4bfa 100644 --- a/vendor/github.com/go-errors/errors/error_backward.go +++ b/vendor/github.com/go-errors/errors/error_backward.go @@ -1,3 +1,4 @@ +//go:build !go1.13 // +build !go1.13 package errors @@ -55,3 +56,70 @@ func Is(e error, original error) bool { return false } + +// Disclaimer: functions Join and Unwrap are copied from the stdlib errors +// package v1.21.0. + +// Join returns an error that wraps the given errors. +// Any nil error values are discarded. +// Join returns nil if every value in errs is nil. +// The error formats as the concatenation of the strings obtained +// by calling the Error method of each element of errs, with a newline +// between each string. +// +// A non-nil error returned by Join implements the Unwrap() []error method. +func Join(errs ...error) error { + n := 0 + for _, err := range errs { + if err != nil { + n++ + } + } + if n == 0 { + return nil + } + e := &joinError{ + errs: make([]error, 0, n), + } + for _, err := range errs { + if err != nil { + e.errs = append(e.errs, err) + } + } + return e +} + +type joinError struct { + errs []error +} + +func (e *joinError) Error() string { + var b []byte + for i, err := range e.errs { + if i > 0 { + b = append(b, '\n') + } + b = append(b, err.Error()...) + } + return string(b) +} + +func (e *joinError) Unwrap() []error { + return e.errs +} + +// Unwrap returns the result of calling the Unwrap method on err, if err's +// type contains an Unwrap method returning error. +// Otherwise, Unwrap returns nil. +// +// Unwrap only calls a method of the form "Unwrap() error". +// In particular Unwrap does not unwrap errors returned by [Join]. +func Unwrap(err error) error { + u, ok := err.(interface { + Unwrap() error + }) + if !ok { + return nil + } + return u.Unwrap() +} diff --git a/vendor/github.com/go-errors/errors/join_unwrap_1_20.go b/vendor/github.com/go-errors/errors/join_unwrap_1_20.go new file mode 100644 index 000000000..44df35ece --- /dev/null +++ b/vendor/github.com/go-errors/errors/join_unwrap_1_20.go @@ -0,0 +1,32 @@ +//go:build go1.20 +// +build go1.20 + +package errors + +import baseErrors "errors" + +// Join returns an error that wraps the given errors. +// Any nil error values are discarded. +// Join returns nil if every value in errs is nil. +// The error formats as the concatenation of the strings obtained +// by calling the Error method of each element of errs, with a newline +// between each string. +// +// A non-nil error returned by Join implements the Unwrap() []error method. +// +// For more information see stdlib errors.Join. +func Join(errs ...error) error { + return baseErrors.Join(errs...) +} + +// Unwrap returns the result of calling the Unwrap method on err, if err's +// type contains an Unwrap method returning error. +// Otherwise, Unwrap returns nil. +// +// Unwrap only calls a method of the form "Unwrap() error". +// In particular Unwrap does not unwrap errors returned by [Join]. +// +// For more information see stdlib errors.Unwrap. +func Unwrap(err error) error { + return baseErrors.Unwrap(err) +} diff --git a/vendor/github.com/go-errors/errors/join_unwrap_backward.go b/vendor/github.com/go-errors/errors/join_unwrap_backward.go new file mode 100644 index 000000000..50c766976 --- /dev/null +++ b/vendor/github.com/go-errors/errors/join_unwrap_backward.go @@ -0,0 +1,71 @@ +//go:build !go1.20 +// +build !go1.20 + +package errors + +// Disclaimer: functions Join and Unwrap are copied from the stdlib errors +// package v1.21.0. + +// Join returns an error that wraps the given errors. +// Any nil error values are discarded. +// Join returns nil if every value in errs is nil. +// The error formats as the concatenation of the strings obtained +// by calling the Error method of each element of errs, with a newline +// between each string. +// +// A non-nil error returned by Join implements the Unwrap() []error method. +func Join(errs ...error) error { + n := 0 + for _, err := range errs { + if err != nil { + n++ + } + } + if n == 0 { + return nil + } + e := &joinError{ + errs: make([]error, 0, n), + } + for _, err := range errs { + if err != nil { + e.errs = append(e.errs, err) + } + } + return e +} + +type joinError struct { + errs []error +} + +func (e *joinError) Error() string { + var b []byte + for i, err := range e.errs { + if i > 0 { + b = append(b, '\n') + } + b = append(b, err.Error()...) + } + return string(b) +} + +func (e *joinError) Unwrap() []error { + return e.errs +} + +// Unwrap returns the result of calling the Unwrap method on err, if err's +// type contains an Unwrap method returning error. +// Otherwise, Unwrap returns nil. +// +// Unwrap only calls a method of the form "Unwrap() error". +// In particular Unwrap does not unwrap errors returned by [Join]. +func Unwrap(err error) error { + u, ok := err.(interface { + Unwrap() error + }) + if !ok { + return nil + } + return u.Unwrap() +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/LICENSE b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types/common.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types/common.go new file mode 100644 index 000000000..b36683911 --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types/common.go @@ -0,0 +1,137 @@ +package types + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" + "time" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/client-go/util/jsonpath" + yaml "sigs.k8s.io/yaml" +) + +// FormattedOutput is a type that contains the formatted data of a resource. +type FormattedOutput struct { + Data string `json:"data"` +} + +// ToJSON gets the JSON data from a resource. +func (j *FormattedOutput) ToJSON(data any) error { + jsonData, err := json.Marshal(data) + if err != nil { + return err + } + j.Data = string(jsonData) + return nil +} + +// ToJSONPath gets the JSONPath data from a resource. +func (j *FormattedOutput) ToJSONPath(template string, data map[string]any) error { + jp := jsonpath.New("jsonpath") + if err := jp.Parse(template); err != nil { + return err + } + dataBuffer := bytes.NewBuffer(nil) + err := jp.Execute(dataBuffer, data) + if err != nil { + return fmt.Errorf("failed to execute jsonpath template %s, error: %w", template, err) + } + j.Data = dataBuffer.String() + return nil +} + +// ToYAML gets the YAML data from a resource. +func (j *FormattedOutput) ToYAML(data any) error { + yamlData, err := yaml.Marshal(data) + if err != nil { + return err + } + j.Data = string(yamlData) + return nil +} + +// NamespacedNameParams is a type that contains the name and namespace of a resource. +type NamespacedNameParams struct { + Name string `json:"name"` + Namespace string `json:"namespace,omitempty"` +} + +// NamespacedNameResult is a type that contains the name and namespace of a resource. +// The fields are optional. +type NamespacedNameResult struct { + Name string `json:"name,omitempty"` + Namespace string `json:"namespace,omitempty"` +} + +// Resource is a type that contains the name, namespace, age, labels and annotations of a resource. +type Resource struct { + NamespacedNameResult + Age string `json:"age,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + Annotations map[string]string `json:"annotations,omitempty"` + FormattedOutput +} + +// GetResourceData gets the data of a resource. If isDetailed is true, the labels and annotations are also included. +func (r *Resource) GetResourceData(resource *unstructured.Unstructured, isDetailed bool) { + r.Name = resource.GetName() + r.Namespace = resource.GetNamespace() + r.Age = FormatAge(time.Since(resource.GetCreationTimestamp().Time)) + + if isDetailed { + r.Labels = resource.GetLabels() + r.Annotations = resource.GetAnnotations() + } +} + +// GroupVersionKind is a type that contains the group, version and kind of a resource. +type GroupVersionKind struct { + Group string `json:"group,omitempty"` + Version string `json:"version"` + Kind string `json:"kind"` +} + +// OutputType is a type that contains the output type of a resource. +type OutputType string + +const ( + // YAMLOutputType is the output type for yaml data. + YAMLOutputType OutputType = "yaml" + // JSONOutputType is the output type for json data. + JSONOutputType OutputType = "json" + // JSONPathOutputType is the output type for jsonpath data. + JSONPathOutputType OutputType = "jsonpath" + // WideOutputType is the output type for detailed data. + WideOutputType OutputType = "wide" +) + +// OutputParams is a type that contains the output type and JSONPathTemplate of a resource. +type OutputParams struct { + // OutputType is the output type of the resource. If set, it can be yaml, json, jsonpath or wide. + // For jsonpath, the template should be provided as part of the output type. + // For example, output_type="jsonpath='{.metadata.name}'". + OutputType OutputType `json:"output_type,omitempty"` +} + +// ValidateOutputParams validates the output parameters. +func (o *OutputParams) ValidateOutputParams() error { + if o.OutputType != "" && o.OutputType != YAMLOutputType && o.OutputType != JSONOutputType && + o.OutputType != WideOutputType && !strings.HasPrefix(string(o.OutputType), string(JSONPathOutputType)+"=") { + return fmt.Errorf("invalid output_type: %s", o.OutputType) + } + if jsonPathTemplate, found := strings.CutPrefix(string(o.OutputType), string(JSONPathOutputType)+"="); found { + err := jsonpath.NewParser("validate").Parse(jsonPathTemplate) + if err != nil { + return fmt.Errorf("invalid json_path_template: %s, error: %w", jsonPathTemplate, err) + } + } + return nil +} + +// GetParams is a type that contains the name, namespace and output type of a resource. +type GetParams struct { + NamespacedNameParams + OutputParams +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types/nodes.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types/nodes.go new file mode 100644 index 000000000..47d606e31 --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types/nodes.go @@ -0,0 +1,16 @@ +package types + +// DebugNodeParams is a type that contains the name, image and command of a node. +type DebugNodeParams struct { + Name string `json:"name"` + Image string `json:"image"` + Command []string `json:"command"` + HostPath string `json:"host_path,omitempty"` + MountPath string `json:"mount_path,omitempty"` +} + +// DebugNodeResult is a type that contains the stdout and stderr of the executed command. +type DebugNodeResult struct { + Stdout string `json:"stdout"` + Stderr string `json:"stderr"` +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types/pods.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types/pods.go new file mode 100644 index 000000000..6231569f6 --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types/pods.go @@ -0,0 +1,34 @@ +package types + +import ( + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/headtail" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/pattern" +) + +// GetPodLogsParams is a type that contains the name, namespace and container of a pod. +type GetPodLogsParams struct { + NamespacedNameParams + Container string `json:"container,omitempty"` + Previous bool `json:"previous,omitempty"` + pattern.PatternParams + headtail.HeadTailParams +} + +// GetPodLogsResult is a type that contains the logs of a pod where each log line +// is a separate element in the string slice. +type GetPodLogsResult struct { + Logs []string `json:"logs"` +} + +// ExecPodParams is a type that contains the name, namespace and container of a pod. +type ExecPodParams struct { + NamespacedNameParams + Container string `json:"container,omitempty"` + Command []string `json:"command"` +} + +// ExecPodResult is a type that contains the stdout and stderr of the executed command. +type ExecPodResult struct { + Stdout string `json:"stdout"` + Stderr string `json:"stderr"` +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types/resources.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types/resources.go new file mode 100644 index 000000000..531275254 --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types/resources.go @@ -0,0 +1,30 @@ +package types + +// GetResourceParams is a type that contains the group, version, kind, name and namespace of a resource. +type GetResourceParams struct { + GroupVersionKind + GetParams +} + +// GetResourceResult is a type that contains the resource data. +type GetResourceResult struct { + Resource Resource `json:"resource"` +} + +// ListParams is a type that contains the namespace, label selector and output type of a resource. +type ListParams struct { + Namespace string `json:"namespace,omitempty"` + LabelSelector string `json:"label_selector,omitempty"` + OutputParams +} + +// ListResourcesParams is a type that contains the group, version, kind, namespace and output type of a resource. +type ListResourcesParams struct { + GroupVersionKind + ListParams +} + +// ListResourcesResult is a type that contains the resource data. +type ListResourcesResult struct { + Resources []Resource `json:"resources"` +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types/utils.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types/utils.go new file mode 100644 index 000000000..3991eea2c --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types/utils.go @@ -0,0 +1,19 @@ +package types + +import ( + "fmt" + "time" +) + +// FormatAge formats the age of a resource in a human readable format. +func FormatAge(age time.Duration) string { + if age < time.Minute { + return fmt.Sprintf("%ds", int64(age.Seconds())) + } else if age < time.Hour { + return fmt.Sprintf("%dm%ds", int64(age.Minutes()), int64(age.Seconds()-float64(int64(age.Minutes())*60))) + } else if age < time.Hour*24 { + return fmt.Sprintf("%dh%dm", int64(age.Hours()), int64(age.Minutes()-float64(int64(age.Hours())*60))) + } else { + return fmt.Sprintf("%dd%dh", int64(age.Hours()/24), int64(age.Hours()-float64(int64(age.Hours()/24))*24)) + } +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovn/mcp/commands.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovn/mcp/commands.go new file mode 100644 index 000000000..4e6228724 --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovn/mcp/commands.go @@ -0,0 +1,49 @@ +package mcp + +import ( + "fmt" + + ovntypes "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovn/types" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils" +) + +const defaultMaxLines = 100 + +// validateDatabase validates that the database is a valid OVN database. +func validateDatabase(db ovntypes.Database) error { + switch db { + case ovntypes.NorthboundDB, ovntypes.SouthboundDB: + return nil + default: + return fmt.Errorf("invalid database %q: must be 'nbdb' or 'sbdb'", db) + } +} + +// validateRecordName validates that a record identifier is safe and non-empty. +func validateRecordName(record string) error { + return utils.ValidateSafeString(record, "record identifier", false, utils.ShellMetaCharactersTypeDefault) +} + +// validateDatapath validates that a datapath name is safe and non-empty. +func validateDatapath(datapath string) error { + return utils.ValidateSafeString(datapath, "datapath name", false, utils.ShellMetaCharactersTypeDefault) +} + +// validateMicroflow validates that a microflow specification is safe and non-empty. +// Microflow specs can contain && for logical AND, so we allow & but block other dangerous chars. +func validateMicroflow(microflow string) error { + return utils.ValidateSafeString(microflow, "microflow specification", false, utils.ShellMetaCharactersTypeAllowBracketsAllowAmp) +} + +// validateColumnSpec validates that a column specification is safe (can be empty). +func validateColumnSpec(columns string) error { + return utils.ValidateSafeString(columns, "column specification", true, utils.ShellMetaCharactersTypeDefault) +} + +// getDBCommand returns the appropriate command (ovn-nbctl or ovn-sbctl) for the given database. +func getDBCommand(db ovntypes.Database) string { + if db == ovntypes.SouthboundDB { + return "ovn-sbctl" + } + return "ovn-nbctl" +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovn/mcp/mcp.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovn/mcp/mcp.go new file mode 100644 index 000000000..afabf2130 --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovn/mcp/mcp.go @@ -0,0 +1,248 @@ +package mcp + +import ( + "context" + "fmt" + "strings" + + "github.com/modelcontextprotocol/go-sdk/mcp" + ovntypes "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovn/types" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/ovndb" +) + +type RunPodExecCommandFuncType func(ctx context.Context, namespace, name, container string, command []string) (string, string, error) + +// MCPServer provides OVN layer analysis tools +type MCPServer struct { + runPodExecCommand RunPodExecCommandFuncType +} + +// NewMCPServer creates a new OVN MCP server +func NewMCPServer(runPodExecCommand RunPodExecCommandFuncType) (*MCPServer, error) { + if runPodExecCommand == nil { + return nil, fmt.Errorf("function to run pod exec command is nil") + } + return &MCPServer{ + runPodExecCommand: runPodExecCommand, + }, nil +} + +// AddTools registers OVN tools with the MCP server. +func (s *MCPServer) AddTools(server *mcp.Server) { + mcp.AddTool(server, ShowTool, s.Show) + mcp.AddTool(server, GetTool, s.Get) + mcp.AddTool(server, LFlowListTool, s.ListLogicalFlows) + mcp.AddTool(server, TraceTool, s.Trace) +} + +// Show displays a comprehensive overview of OVN configuration. +func (s *MCPServer) Show(ctx context.Context, req *mcp.CallToolRequest, + in ovntypes.ShowParams) (*mcp.CallToolResult, ovntypes.ShowResult, error) { + result := ovntypes.ShowResult{ + Database: in.Database, + } + + // Validate database + if err := validateDatabase(in.Database); err != nil { + return nil, result, err + } + + // Build command + cmd := getDBCommand(in.Database) + stdout, stderr, err := s.runPodExecCommand(ctx, in.Namespace, in.Name, "", []string{cmd, "show"}) + if err != nil { + return nil, result, fmt.Errorf("failed to retrieve OVN configuration from pod %s/%s: %w", + in.Namespace, in.Name, err) + } + if stderr != "" { + return nil, result, fmt.Errorf("failed to retrieve OVN configuration from pod %s/%s: %s", + in.Namespace, in.Name, stderr) + } + lines := utils.StripEmptyLines(strings.Split(stdout, "\n")) + + // Apply the head and tail parameters to the lines + lines = in.HeadTailParams.Apply(lines, defaultMaxLines) + + // Join all lines into a single output string + result.Output = strings.Join(lines, "\n") + return nil, result, nil +} + +// Get queries records from an OVN table with flexible filtering. +// Supports two modes: +// 1. List all records (when Record is empty) +// 2. Get specific record (when Record is set) +// Both modes support filtering columns with the Columns parameter. +func (s *MCPServer) Get(ctx context.Context, req *mcp.CallToolRequest, + in ovntypes.GetParams) (*mcp.CallToolResult, ovntypes.GetResult, error) { + result := ovntypes.GetResult{ + Database: in.Database, + Table: in.Table, + Record: in.Record, + } + + // Validate inputs + if err := validateDatabase(in.Database); err != nil { + return nil, result, err + } + if err := ovndb.ValidateOVNTableName(in.Table); err != nil { + return nil, result, err + } + if err := validateColumnSpec(in.Columns); err != nil { + return nil, result, err + } + + cmd := getDBCommand(in.Database) + cmdArgs := []string{cmd} + + // Add columns filter if specified + if in.Columns != "" { + cmdArgs = append(cmdArgs, "--columns="+in.Columns) + } + + if in.Record == "" { + // Mode 1: List all records in the table + cmdArgs = append(cmdArgs, "list", in.Table) + } else { + // Mode 2: Get specific record + if err := validateRecordName(in.Record); err != nil { + return nil, result, err + } + cmdArgs = append(cmdArgs, "list", in.Table, in.Record) + } + + // Match the pattern to the get results if in list mode + lines, err := in.PatternParams.ExecuteWithMatch(func() ([]string, error) { + stdout, stderr, err := s.runPodExecCommand(ctx, in.Namespace, in.Name, "", cmdArgs) + if err != nil { + if in.Record != "" { + return nil, fmt.Errorf("failed to get record %s from table %s on pod %s/%s: %w", + in.Record, in.Table, in.Namespace, in.Name, err) + } + return nil, fmt.Errorf("failed to list table %s from pod %s/%s: %w", + in.Table, in.Namespace, in.Name, err) + } + if stderr != "" { + if in.Record != "" { + return nil, fmt.Errorf("failed to get record %s from table %s on pod %s/%s: %s", + in.Record, in.Table, in.Namespace, in.Name, stderr) + } + return nil, fmt.Errorf("failed to list table %s from pod %s/%s: %s", + in.Table, in.Namespace, in.Name, stderr) + } + lines := utils.StripEmptyLines(strings.Split(stdout, "\n")) + return lines, nil + }, in.Record == "") + if err != nil { + return nil, result, err + } + + // Apply the head and tail parameters to the lines + lines = in.HeadTailParams.Apply(lines, defaultMaxLines) + + result.Output = strings.Join(lines, "\n") + return nil, result, nil +} + +// ListLogicalFlows lists logical flows from the Southbound database. +func (s *MCPServer) ListLogicalFlows(ctx context.Context, req *mcp.CallToolRequest, + in ovntypes.LogicalFlowListParams) (*mcp.CallToolResult, ovntypes.LogicalFlowListResult, error) { + result := ovntypes.LogicalFlowListResult{ + Datapath: in.Datapath, + Flows: []string{}, + } + + // Validate datapath if provided + if in.Datapath != "" { + if err := validateDatapath(in.Datapath); err != nil { + return nil, result, err + } + } + + // Build command + cmdArgs := []string{"ovn-sbctl", "lflow-list"} + if in.Datapath != "" { + cmdArgs = append(cmdArgs, in.Datapath) + } + + // Match the pattern to the logical flows + lines, err := in.PatternParams.ExecuteWithMatch(func() ([]string, error) { + stdout, stderr, err := s.runPodExecCommand(ctx, in.Namespace, in.Name, "", cmdArgs) + if err != nil { + return nil, fmt.Errorf("failed to list logical flows from pod %s/%s: %w", + in.Namespace, in.Name, err) + } + if stderr != "" { + return nil, fmt.Errorf("failed to list logical flows from pod %s/%s: %s", + in.Namespace, in.Name, stderr) + } + lines := utils.StripEmptyLines(strings.Split(stdout, "\n")) + return lines, nil + }, true) + if err != nil { + return nil, result, err + } + + // Apply the head and tail parameters to the lines + lines = in.HeadTailParams.Apply(lines, defaultMaxLines) + + result.Flows = lines + return nil, result, nil +} + +// Trace traces a packet through the OVN logical network. +func (s *MCPServer) Trace(ctx context.Context, req *mcp.CallToolRequest, + in ovntypes.OVNTraceParams) (*mcp.CallToolResult, ovntypes.OVNTraceResult, error) { + result := ovntypes.OVNTraceResult{ + Datapath: in.Datapath, + Microflow: in.Microflow, + } + + // Validate inputs + if err := validateDatapath(in.Datapath); err != nil { + return nil, result, err + } + if err := validateMicroflow(in.Microflow); err != nil { + return nil, result, err + } + + // Build command: ovn-trace '' + cmdArgs := []string{"ovn-trace"} + + // Add output format flag based on mode (default to detailed) + switch in.Mode { + case ovntypes.TraceModeSummary: + cmdArgs = append(cmdArgs, "--summary") + case ovntypes.TraceModeMinimal: + cmdArgs = append(cmdArgs, "--minimal") + case ovntypes.TraceModeDetailed, "": + cmdArgs = append(cmdArgs, "--detailed") + } + + cmdArgs = append(cmdArgs, in.Datapath, in.Microflow) + + // Match the pattern to the trace output + lines, err := in.PatternParams.ExecuteWithMatch(func() ([]string, error) { + stdout, stderr, err := s.runPodExecCommand(ctx, in.Namespace, in.Name, "", cmdArgs) + if err != nil { + return nil, fmt.Errorf("failed to trace packet on pod %s/%s: %w", + in.Namespace, in.Name, err) + } + if stderr != "" { + return nil, fmt.Errorf("failed to trace packet on pod %s/%s: %s", + in.Namespace, in.Name, stderr) + } + lines := utils.StripEmptyLines(strings.Split(stdout, "\n")) + return lines, nil + }, true) + if err != nil { + return nil, result, err + } + + // Apply the head and tail parameters to the lines + lines = in.HeadTailParams.Apply(lines, defaultMaxLines) + + result.Output = strings.Join(lines, "\n") + return nil, result, nil +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovn/mcp/tools.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovn/mcp/tools.go new file mode 100644 index 000000000..57907680f --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovn/mcp/tools.go @@ -0,0 +1,294 @@ +package mcp + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/google/jsonschema-go/jsonschema" + gosdk "github.com/modelcontextprotocol/go-sdk/mcp" + "k8s.io/utils/ptr" + + ovntypes "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovn/types" +) + +// ExecTarget represents the logical execution target for an OVN tool. +// Upstream defines what role it needs; downstream maps to concrete containers. +type ExecTarget string + +const ( + TargetNBDB ExecTarget = "nbdb" + TargetSBDB ExecTarget = "sbdb" + TargetNorthd ExecTarget = "northd" +) + +// ExecuteFunc is a unified execution function for OVN tools. +type ExecuteFunc func(s *MCPServer, ctx context.Context, args map[string]any) (any, error) + +// ToolRegistration pairs a tool definition with its execution logic and +// routing metadata. +type ToolRegistration struct { + Tool *gosdk.Tool + Execute ExecuteFunc + TargetSelector func(args map[string]any) ExecTarget +} + +// MakeExecute creates an ExecuteFunc from a typed MCPServer method. +// Passes a synthetic *CallToolRequest (with populated Arguments) to avoid +// nil-dereference risk if upstream ever uses req. +func MakeExecute[P any, R any]( + method func(*MCPServer, context.Context, *gosdk.CallToolRequest, P) (*gosdk.CallToolResult, R, error), +) ExecuteFunc { + return func(s *MCPServer, ctx context.Context, args map[string]any) (any, error) { + var p P + if err := unmarshalArgs(args, &p); err != nil { + return nil, err + } + data, err := json.Marshal(args) + if err != nil { + return nil, fmt.Errorf("failed to marshal arguments for request: %w", err) + } + req := &gosdk.CallToolRequest{Params: &gosdk.CallToolParamsRaw{Arguments: data}} + _, result, err := method(s, ctx, req, p) + return result, err + } +} + +// Exported tool definitions, shared between AddTools and AllToolRegistrations. +var ( + ShowTool = showTool() + GetTool = getTool() + LFlowListTool = lflowListTool() + TraceTool = traceTool() +) + +// AllToolRegistrations returns all OVN tool registrations. +func AllToolRegistrations() []ToolRegistration { + return []ToolRegistration{ + {Tool: ShowTool, Execute: MakeExecute((*MCPServer).Show), TargetSelector: dbTarget}, + {Tool: GetTool, Execute: MakeExecute((*MCPServer).Get), TargetSelector: dbTarget}, + {Tool: LFlowListTool, Execute: MakeExecute((*MCPServer).ListLogicalFlows), TargetSelector: staticTarget(TargetSBDB)}, + {Tool: TraceTool, Execute: MakeExecute((*MCPServer).Trace), TargetSelector: staticTarget(TargetNorthd)}, + } +} + +func dbTarget(args map[string]any) ExecTarget { + if db, _ := args["database"].(string); db == string(ovntypes.SouthboundDB) { + return TargetSBDB + } + return TargetNBDB +} + +func staticTarget(t ExecTarget) func(map[string]any) ExecTarget { + return func(_ map[string]any) ExecTarget { return t } +} + +func unmarshalArgs(args map[string]any, dest any) error { + data, err := json.Marshal(args) + if err != nil { + return fmt.Errorf("failed to marshal arguments: %w", err) + } + if err := json.Unmarshal(data, dest); err != nil { + return fmt.Errorf("failed to unmarshal arguments: %w", err) + } + return nil +} + +var headTailSchema = map[string]*jsonschema.Schema{ + "head": { + Type: "integer", + Description: "Return only first N lines (default: 100 if tail is not specified)", + Minimum: ptr.To(float64(1)), + }, + "tail": { + Type: "integer", + Description: "Return only last N lines", + Minimum: ptr.To(float64(1)), + }, + "apply_tail_first": { + Type: "boolean", + Description: "If both head and tail are set and apply_tail_first is true, apply tail before head (default: false)", + }, +} + +func mergeProps(base map[string]*jsonschema.Schema, extra map[string]*jsonschema.Schema) map[string]*jsonschema.Schema { + for k, v := range extra { + base[k] = v + } + return base +} + +func showTool() *gosdk.Tool { + return &gosdk.Tool{ + Name: "ovn_show", + Description: `Display a comprehensive overview of OVN configuration from either the Northbound or Southbound database. + +For Northbound (nbdb): Runs 'ovn-nbctl show' and displays logical switches, logical routers, their ports, and connections between them. +For Southbound (sbdb): Runs 'ovn-sbctl show' and displays chassis information, port bindings, and their relationships.`, + Title: "OVN: Show", + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: mergeProps(map[string]*jsonschema.Schema{ + "namespace": { + Type: "string", + Description: "Kubernetes namespace of the OVN pod (e.g., \"openshift-ovn-kubernetes\")", + }, + "name": { + Type: "string", + Description: "Name of the pod running OVN (e.g., \"ovnkube-node-xxxxx\")", + }, + "database": { + Type: "string", + Description: `OVN database to query - "nbdb" for Northbound or "sbdb" for Southbound`, + Enum: []any{string(ovntypes.NorthboundDB), string(ovntypes.SouthboundDB)}, + }, + }, headTailSchema), + Required: []string{"namespace", "name", "database"}, + }, + Annotations: &gosdk.ToolAnnotations{ + ReadOnlyHint: true, + IdempotentHint: true, + OpenWorldHint: ptr.To(true), + }, + } +} + +func getTool() *gosdk.Tool { + return &gosdk.Tool{ + Name: "ovn_get", + Description: `Query records from an OVN database table with flexible filtering. + +Can list all records in a table (when no record specified) or get a specific record (when record specified). + +Common Northbound tables: Logical_Switch, Logical_Router, Logical_Switch_Port, Logical_Router_Port, ACL, Address_Set, Port_Group, Load_Balancer, NAT. +Common Southbound tables: Chassis, Port_Binding, Datapath_Binding, Logical_Flow, MAC_Binding, Multicast_Group, SB_Global.`, + Title: "OVN: Get", + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: mergeProps(map[string]*jsonschema.Schema{ + "namespace": { + Type: "string", + Description: "Kubernetes namespace of the OVN pod", + }, + "name": { + Type: "string", + Description: "Name of the pod running OVN", + }, + "database": { + Type: "string", + Description: `OVN database to query - "nbdb" for Northbound or "sbdb" for Southbound`, + Enum: []any{string(ovntypes.NorthboundDB), string(ovntypes.SouthboundDB)}, + }, + "table": { + Type: "string", + Description: "Name of the OVN table (e.g., \"Logical_Switch\", \"Port_Binding\")", + }, + "record": { + Type: "string", + Description: "Record identifier (UUID or name). If not specified, lists all records", + }, + "columns": { + Type: "string", + Description: "Comma-separated list of columns to display (e.g., \"name,_uuid,ports\")", + }, + "pattern": { + Type: "string", + Description: "Regex pattern to filter results (only applies when listing all records)", + }, + }, headTailSchema), + Required: []string{"namespace", "name", "database", "table"}, + }, + Annotations: &gosdk.ToolAnnotations{ + ReadOnlyHint: true, + IdempotentHint: true, + OpenWorldHint: ptr.To(true), + }, + } +} + +func lflowListTool() *gosdk.Tool { + return &gosdk.Tool{ + Name: "ovn_lflow_list", + Description: `List logical flows from the OVN Southbound database. + +Runs 'ovn-sbctl lflow-list' to retrieve logical flows which represent the compiled logical network pipeline. Essential for debugging packet forwarding.`, + Title: "OVN: Logical Flow List", + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: mergeProps(map[string]*jsonschema.Schema{ + "namespace": { + Type: "string", + Description: "Kubernetes namespace of the OVN pod", + }, + "name": { + Type: "string", + Description: "Name of the pod running OVN", + }, + "datapath": { + Type: "string", + Description: "Datapath name or UUID to filter flows for a specific logical switch/router", + }, + "pattern": { + Type: "string", + Description: "Regex pattern to filter flows", + }, + }, headTailSchema), + Required: []string{"namespace", "name"}, + }, + Annotations: &gosdk.ToolAnnotations{ + ReadOnlyHint: true, + IdempotentHint: true, + OpenWorldHint: ptr.To(true), + }, + } +} + +func traceTool() *gosdk.Tool { + return &gosdk.Tool{ + Name: "ovn_trace", + Description: `Trace a packet through the OVN logical network. + +Runs 'ovn-trace' to simulate packet processing through the logical network pipeline. Shows which logical flows match, what actions are taken, and the final disposition. Essential for debugging connectivity issues. + +Microflow examples: +- inport=="pod1" && eth.src==00:00:00:00:00:01 && ip4.src==10.244.0.5 && ip4.dst==10.244.1.5 +- inport=="pod1" && eth.src==00:00:00:00:00:01 && icmp && ip4.src==10.244.0.5 && ip4.dst==8.8.8.8`, + Title: "OVN: Trace", + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: mergeProps(map[string]*jsonschema.Schema{ + "namespace": { + Type: "string", + Description: "Kubernetes namespace of the OVN pod", + }, + "name": { + Type: "string", + Description: "Name of the pod running OVN", + }, + "datapath": { + Type: "string", + Description: "Name of the logical switch or router to start the trace", + }, + "microflow": { + Type: "string", + Description: "Microflow specification describing the packet to trace", + }, + "mode": { + Type: "string", + Description: "Output verbosity mode (default: \"detailed\")", + Enum: []any{"detailed", "summary", "minimal"}, + }, + "pattern": { + Type: "string", + Description: "Regex pattern to filter trace output", + }, + }, headTailSchema), + Required: []string{"namespace", "name", "datapath", "microflow"}, + }, + Annotations: &gosdk.ToolAnnotations{ + ReadOnlyHint: true, + IdempotentHint: true, + OpenWorldHint: ptr.To(true), + }, + } +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovn/types/command.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovn/types/command.go new file mode 100644 index 000000000..c9329a708 --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovn/types/command.go @@ -0,0 +1,96 @@ +package types + +import ( + k8stypes "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/headtail" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/pattern" +) + +// Database represents an OVN database type. +type Database string + +const ( + // NorthboundDB is the OVN Northbound database. + NorthboundDB Database = "nbdb" + // SouthboundDB is the OVN Southbound database. + SouthboundDB Database = "sbdb" +) + +// ShowParams are the parameters for ovn-nbctl/ovn-sbctl show command. +type ShowParams struct { + k8stypes.NamespacedNameParams + Database Database `json:"database"` + headtail.HeadTailParams +} + +// ShowResult contains the output of ovn-nbctl/ovn-sbctl show command. +type ShowResult struct { + Database Database `json:"database"` + Output string `json:"output"` +} + +// LogicalFlowListParams are the parameters for listing logical flows from SBDB. +type LogicalFlowListParams struct { + k8stypes.NamespacedNameParams + Datapath string `json:"datapath,omitempty"` + pattern.PatternParams + headtail.HeadTailParams +} + +// LogicalFlowListResult contains the list of logical flows. +type LogicalFlowListResult struct { + Datapath string `json:"datapath,omitempty"` + Flows []string `json:"flows"` +} + +// TraceMode represents the output verbosity mode for ovn-trace. +type TraceMode string + +const ( + // TraceModeDetailed shows detailed trace output (default). + TraceModeDetailed TraceMode = "detailed" + // TraceModeSummary shows summary output only. + TraceModeSummary TraceMode = "summary" + // TraceModeMinimal shows minimal output. + TraceModeMinimal TraceMode = "minimal" +) + +// OVNTraceParams are the parameters for ovn-trace command. +type OVNTraceParams struct { + k8stypes.NamespacedNameParams + Datapath string `json:"datapath"` + Microflow string `json:"microflow"` + Mode TraceMode `json:"mode,omitempty"` // Output mode: detailed (default), summary, or minimal + pattern.PatternParams + headtail.HeadTailParams +} + +// OVNTraceResult contains the output of ovn-trace command. +type OVNTraceResult struct { + Datapath string `json:"datapath"` + Microflow string `json:"microflow"` + Output string `json:"output"` +} + +// GetParams are the parameters for querying records from an OVN table. +// This is a flexible command that supports: +// - Listing all records (when Record is empty) +// - Getting a specific record (when Record is set) +// - Getting specific columns (when Columns is set) +type GetParams struct { + k8stypes.NamespacedNameParams + Database Database `json:"database"` + Table string `json:"table"` + Record string `json:"record,omitempty"` // Optional: if empty, lists all records + Columns string `json:"columns,omitempty"` // Optional: comma-separated columns to retrieve + pattern.PatternParams + headtail.HeadTailParams +} + +// GetResult contains the output of ovn-nbctl/ovn-sbctl query. +type GetResult struct { + Database Database `json:"database"` + Table string `json:"table"` + Record string `json:"record,omitempty"` + Output string `json:"output"` +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/headtail/head_tail.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/headtail/head_tail.go new file mode 100644 index 000000000..4cc821a93 --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/headtail/head_tail.go @@ -0,0 +1,61 @@ +package headtail + +// HeadTailParams is a type that contains the head and tail parameters. +type HeadTailParams struct { + Head int `json:"head,omitempty"` + Tail int `json:"tail,omitempty"` + ApplyTailFirst bool `json:"apply_tail_first,omitempty"` +} + +// Apply applies the head and tail parameters to the lines. If none +// are set, then only head is applied and the default maximum +// number of lines is returned. If both Head and Tail are set, and +// ApplyTailFirst is true, tail will be applied first, otherwise +// head will be applied first. If only one of Head or Tail is set, +// that one will be applied and ApplyTailFirst will be ignored. +func (h *HeadTailParams) Apply(lines []string, defaultMaxLines int) []string { + // If neither Head nor Tail is set, return the default maximum number of lines. + if h.Head == 0 && h.Tail == 0 { + return head(lines, defaultMaxLines) + } + // If both Head and Tail are set, apply them in the order specified by ApplyTailFirst. + if h.Head != 0 && h.Tail != 0 { + if h.ApplyTailFirst { + return head(tail(lines, h.Tail), h.Head) + } else { + return tail(head(lines, h.Head), h.Tail) + } + } + // If only Head is set, apply it. + if h.Head != 0 { + return head(lines, h.Head) + } + // If only Tail is set, apply it. + return tail(lines, h.Tail) +} + +// head returns the first n lines of a slice of strings. It will return a new slice of strings +// with the first n lines. If n is less than or equal to 0, or greater than or equal to the +// length of the slice, it will return the entire slice. +func head(lines []string, n int) []string { + if len(lines) == 0 { + return lines + } + if n <= 0 || n >= len(lines) { + return lines + } + return lines[:n] +} + +// tail returns the last n lines of a slice of strings. It will return a new slice of strings +// with the last n lines. If n is less than or equal to 0, or greater than or equal to the +// length of the slice, it will return the entire slice. +func tail(lines []string, n int) []string { + if len(lines) == 0 { + return lines + } + if n <= 0 || n >= len(lines) { + return lines + } + return lines[len(lines)-n:] +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/ovndb/ovn_db.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/ovndb/ovn_db.go new file mode 100644 index 000000000..edc1d3ec5 --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/ovndb/ovn_db.go @@ -0,0 +1,24 @@ +package ovndb + +import ( + "fmt" + "regexp" +) + +// validOVNTableNamePattern matches valid OVN table names: start with letter, alphanumeric +// and underscores. +var validOVNTableNamePattern = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_]*$`) + +// ValidateOVNTableName validates that a table name is safe and non-empty. +// Table names should only contain alphanumeric characters and underscores. +func ValidateOVNTableName(table string) error { + if table == "" { + return fmt.Errorf("table name cannot be empty") + } + + if !validOVNTableNamePattern.MatchString(table) { + return fmt.Errorf("invalid table name %q: must start with a letter and contain only alphanumeric characters and underscores", table) + } + + return nil +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/pattern/pattern.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/pattern/pattern.go new file mode 100644 index 000000000..af84ff9d0 --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/pattern/pattern.go @@ -0,0 +1,45 @@ +package pattern + +import ( + "fmt" + "regexp" +) + +type PatternParams struct { + Pattern string `json:"pattern,omitempty"` +} + +// ExecuteWithMatch executes a function and matches the output to the pattern. +// If checkMatch is false, a non-empty Pattern is rejected with an error because +// pattern matching is not supported in that mode; an empty Pattern returns f() +// unchanged. If checkMatch is true, it returns f() when Pattern is empty, or the +// regex-matched subset of f()'s lines when Pattern is set. An error is returned +// if the pattern is invalid or if f() fails. +func (p *PatternParams) ExecuteWithMatch(f func() ([]string, error), checkMatch bool) ([]string, error) { + if !checkMatch { + if p.Pattern != "" { + return nil, fmt.Errorf("pattern matching is not supported in this mode (got pattern %q)", p.Pattern) + } + return f() + } + if p.Pattern == "" { + return f() + } + searchPattern, err := regexp.Compile(p.Pattern) + if err != nil { + return nil, fmt.Errorf("invalid search pattern %q: %w", p.Pattern, err) + } + + lines, err := f() + if err != nil { + return nil, err + } + + matchedLines := []string{} + for _, line := range lines { + if searchPattern.MatchString(line) { + matchedLines = append(matchedLines, line) + } + } + return matchedLines, nil +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/utils.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/utils.go new file mode 100644 index 000000000..78ad19e5c --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/utils.go @@ -0,0 +1,162 @@ +package utils + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + "slices" + "strings" + "unicode/utf8" +) + +var ( + // shellMetaCharacters is the pattern for shell metacharacters. + shellMetaCharacters = regexp.MustCompile(`[;&|$` + "`" + `<>\\()]`) + + // shellMetaCharactersNoBrackets is like shellMetaCharacters but allows brackets. + shellMetaCharactersNoBrackets = regexp.MustCompile(`[;&|$` + "`" + `<>\\]`) + + // shellMetaCharactersNoBracketsNoAmp is like shellMetaCharactersNoBrackets but allows & for + // commands which use && for logical AND operations. + shellMetaCharactersNoBracketsNoAmp = regexp.MustCompile(`[;|$` + "`" + `<>\\]`) + + // shellMetaCharactersNoBracketsSpecialCharacters is like shellMetaCharactersNoBrackets but also + // disallows newline, NUL byte, and single quote ('). + shellMetaCharactersNoBracketsSpecialCharacters = regexp.MustCompile(`[;&|$` + "`" + `\n\x00` + `'<>\\]`) + + // pathUnsafeChar matches the first rune not allowed in a mount path (alphanumeric, /, -, _, ., ~). + pathUnsafeChar = regexp.MustCompile(`[^a-zA-Z0-9/_.~-]`) +) + +// ShellMetaCharactersType is the type of shell metacharacters to validate. +type ShellMetaCharactersType string + +// ShellMetaCharactersType values. +const ( + ShellMetaCharactersTypeDefault ShellMetaCharactersType = "default" + ShellMetaCharactersTypeAllowBrackets ShellMetaCharactersType = "allow_brackets" + ShellMetaCharactersTypeAllowBracketsAllowAmp ShellMetaCharactersType = "allow_brackets_and_amp" + ShellMetaCharactersTypeDisallowSpecialCharacters ShellMetaCharactersType = "disallow_special_characters" +) + +// StripEmptyLines strips empty lines from a slice of strings. It will return a new slice of strings +// with the empty lines removed. +func StripEmptyLines(lines []string) []string { + if len(lines) == 0 { + return lines + } + result := []string{} + for _, line := range lines { + if strings.TrimSpace(line) != "" { + result = append(result, line) + } + } + return result +} + +// GetGitRepositoryRoot returns the root directory of the git repository. It will return an error +// if the current directory is not a git repository or the root directory cannot be found. +func GetGitRepositoryRoot() (string, error) { + currentDir, err := os.Getwd() + if err != nil { + return "", fmt.Errorf("failed to get current directory: %w", err) + } + + for { + gitDirPath := filepath.Join(currentDir, ".git") + if _, err := os.Stat(gitDirPath); err == nil { + return currentDir, nil // Found .git directory, this is the root + } + + parentDir := filepath.Dir(currentDir) + if parentDir == currentDir { // Reached the file system root + return "", fmt.Errorf("failed to find git repository root") + } + currentDir = parentDir + } +} + +// validateShellMetacharacters validates whether the given parameter contains shell +// metacharacters or not. It returns an error if there are any shell metacharacters. +// If shellMetaCharactersType is ShellMetaCharactersTypeDefault, no shell metacharacters are allowed. +// If shellMetaCharactersType is ShellMetaCharactersTypeAllowBracketsAllowAmp, the ( and ) characters and the & character are allowed. +// If shellMetaCharactersType is ShellMetaCharactersTypeAllowBrackets, the ( and ) characters are allowed. +// If shellMetaCharactersType is ShellMetaCharactersTypeDisallowSpecialCharacters, the new line, null byte, +// and special characters are disallowed. +func validateShellMetacharacters(param string, shellMetaCharactersType ShellMetaCharactersType) error { + switch shellMetaCharactersType { + case ShellMetaCharactersTypeAllowBracketsAllowAmp: + if shellMetaCharactersNoBracketsNoAmp.MatchString(param) { + return fmt.Errorf("invalid use of metacharacters in parameter: %s", param) + } + case ShellMetaCharactersTypeAllowBrackets: + if shellMetaCharactersNoBrackets.MatchString(param) { + return fmt.Errorf("invalid use of metacharacters in parameter: %s", param) + } + case ShellMetaCharactersTypeDisallowSpecialCharacters: + if shellMetaCharactersNoBracketsSpecialCharacters.MatchString(param) { + return fmt.Errorf("invalid use of metacharacters in parameter: %s", param) + } + case ShellMetaCharactersTypeDefault: + if shellMetaCharacters.MatchString(param) { + return fmt.Errorf("invalid use of metacharacters in parameter: %s", param) + } + default: + return fmt.Errorf("invalid shell metacharacters type: %s", shellMetaCharactersType) + } + + return nil +} + +// ValidateSafeString is same as validateShellMetacharacters but it also checks if the string is empty. +// If allowEmpty is true, an empty string is allowed and no error is returned. +func ValidateSafeString(value, fieldName string, allowEmpty bool, shellMetaCharactersType ShellMetaCharactersType) error { + if value == "" { + if allowEmpty { + return nil + } + return fmt.Errorf("%s cannot be empty", fieldName) + } + + if err := validateShellMetacharacters(value, shellMetaCharactersType); err != nil { + return fmt.Errorf("invalid %s: contains potentially dangerous characters: %w", fieldName, err) + } + return nil +} + +// ValidatePath validates that a path is safe to use as a filesystem path. +// It ensures the path: +// - Is absolute (starts with /) +// - Does not contain path traversal patterns (..) +// - Contains only safe characters +func ValidatePath(path, pathType string, allowEmpty bool) error { + if path == "" { + if allowEmpty { + return nil + } + return fmt.Errorf("%s cannot be empty", pathType) + } + + // Ensure path is absolute + if !filepath.IsAbs(path) { + return fmt.Errorf("%s must be an absolute path (start with /), got: %s", pathType, path) + } + + // Check for path traversal patterns: reject any path element that is exactly ".." + if slices.Contains(strings.Split(path, string(filepath.Separator)), "..") { + return fmt.Errorf("%s contains path traversal element '..': %s", pathType, path) + } + + // Reject null bytes, control characters, shell specials, and other disallowed runes. + if loc := pathUnsafeChar.FindStringIndex(path); loc != nil { + i := loc[0] + r, size := utf8.DecodeRuneInString(path[i:]) + if r == utf8.RuneError && size <= 1 { + return fmt.Errorf("%s contains invalid/unsafe byte at position %d: 0x%02X", pathType, i, path[i]) + } + return fmt.Errorf("%s contains unsafe character at position %d: %c (U+%04X)", pathType, i, r, r) + } + + return nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index eab109c84..05a7bc165 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -108,7 +108,7 @@ github.com/fsnotify/fsnotify/internal # github.com/fxamacker/cbor/v2 v2.9.0 ## explicit; go 1.20 github.com/fxamacker/cbor/v2 -# github.com/go-errors/errors v1.4.2 +# github.com/go-errors/errors v1.5.1 ## explicit; go 1.14 github.com/go-errors/errors # github.com/go-gorp/gorp/v3 v3.1.0 @@ -410,6 +410,15 @@ github.com/opencontainers/go-digest ## explicit; go 1.18 github.com/opencontainers/image-spec/specs-go github.com/opencontainers/image-spec/specs-go/v1 +# github.com/ovn-kubernetes/ovn-kubernetes-mcp v0.0.0-20260521143347-5fe0e0972cd5 +## explicit; go 1.25.0 +github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types +github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovn/mcp +github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovn/types +github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils +github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/headtail +github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/ovndb +github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/pattern # github.com/peterbourgon/diskv v2.0.1+incompatible ## explicit github.com/peterbourgon/diskv