Skip to content

Commit 5553a4e

Browse files
committed
fix(nodes): nodes_log query and tailLines arguments
Signed-off-by: Marc Nuri <[email protected]>
1 parent 14fa2b3 commit 5553a4e

File tree

10 files changed

+115
-94
lines changed

10 files changed

+115
-94
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,11 @@ In case multi-cluster support is enabled (default) and you have access to multip
235235

236236
- **projects_list** - List all the OpenShift projects in the current cluster
237237

238+
- **nodes_log** - Get logs from a Kubernetes node (kubelet, kube-proxy, or other system logs). This accesses node logs through the Kubernetes API proxy to the kubelet
239+
- `name` (`string`) **(required)** - Name of the node to get logs from
240+
- `query` (`string`) **(required)** - query specifies services(s) or files from which to return logs (required). Example: "kubelet" to fetch kubelet logs, "/<log-file-name>" to fetch a specific log file from the node (e.g., "/var/log/kubelet.log" or "/var/log/kube-proxy.log")
241+
- `tailLines` (`integer`) - Number of lines to retrieve from the end of the logs (Optional, 0 means all logs)
242+
238243
- **pods_list** - List all the Kubernetes pods in the current cluster from all namespaces
239244
- `labelSelector` (`string`) - Optional Kubernetes label selector (e.g. 'app=myapp,env=prod' or 'app in (myapp,yourapp)'), use this option when you want to filter the pods by label
240245

pkg/kubernetes/accesscontrol_clientset.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func (a *AccessControlClientset) DiscoveryClient() discovery.DiscoveryInterface
3939
return a.discoveryClient
4040
}
4141

42-
func (a *AccessControlClientset) NodesLogs(ctx context.Context, name, logPath string) (*rest.Request, error) {
42+
func (a *AccessControlClientset) NodesLogs(ctx context.Context, name string) (*rest.Request, error) {
4343
gvk := &schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Node"}
4444
if !isAllowed(a.staticConfig, gvk) {
4545
return nil, isNotAllowedError(gvk)
@@ -52,8 +52,7 @@ func (a *AccessControlClientset) NodesLogs(ctx context.Context, name, logPath st
5252
url := []string{"api", "v1", "nodes", name, "proxy", "logs"}
5353
return a.delegate.CoreV1().RESTClient().
5454
Get().
55-
AbsPath(url...).
56-
Param("query", logPath), nil
55+
AbsPath(url...), nil
5756
}
5857

5958
func (a *AccessControlClientset) Pods(namespace string) (corev1.PodInterface, error) {

pkg/kubernetes/nodes.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,23 @@ import (
55
"fmt"
66
)
77

8-
func (k *Kubernetes) NodesLog(ctx context.Context, name string, logPath string, tail int64) (string, error) {
8+
func (k *Kubernetes) NodesLog(ctx context.Context, name string, query string, tailLines int64) (string, error) {
99
// Use the node proxy API to access logs from the kubelet
10+
// https://kubernetes.io/docs/concepts/cluster-administration/system-logs/#log-query
1011
// Common log paths:
1112
// - /var/log/kubelet.log - kubelet logs
1213
// - /var/log/kube-proxy.log - kube-proxy logs
1314
// - /var/log/containers/ - container logs
1415

15-
req, err := k.AccessControlClientset().NodesLogs(ctx, name, logPath)
16+
req, err := k.AccessControlClientset().NodesLogs(ctx, name)
1617
if err != nil {
1718
return "", err
1819
}
1920

21+
req.Param("query", query)
2022
// Query parameters for tail
21-
if tail > 0 {
22-
req.Param("tailLines", fmt.Sprintf("%d", tail))
23+
if tailLines > 0 {
24+
req.Param("tailLines", fmt.Sprintf("%d", tailLines))
2325
}
2426

2527
result := req.Do(ctx)

pkg/mcp/nodes_test.go

Lines changed: 55 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package mcp
22

33
import (
44
"net/http"
5+
"strconv"
56
"testing"
67

78
"github.com/BurntSushi/toml"
@@ -43,29 +44,27 @@ func (s *NodesSuite) TestNodesLog() {
4344
}`))
4445
return
4546
}
46-
// Check for log proxy requests based on path and query parameters
47+
// Get Proxy Logs
4748
if req.URL.Path == "/api/v1/nodes/existing-node/proxy/logs" {
48-
logPath := req.URL.Query().Get("query")
49-
50-
// Get Empty Log response
51-
if logPath == "empty.log" {
52-
w.Header().Set("Content-Type", "text/plain")
53-
w.WriteHeader(http.StatusOK)
54-
_, _ = w.Write([]byte(``))
49+
w.Header().Set("Content-Type", "text/plain")
50+
query := req.URL.Query().Get("query")
51+
var logContent string
52+
switch query {
53+
case "/empty.log":
54+
logContent = ""
55+
case "/kubelet.log":
56+
logContent = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\n"
57+
default:
58+
w.WriteHeader(http.StatusNotFound)
5559
return
5660
}
57-
58-
// Get Kubelet Log response
59-
if logPath == "kubelet.log" {
60-
w.Header().Set("Content-Type", "text/plain")
61-
w.WriteHeader(http.StatusOK)
62-
logContent := "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\n"
63-
if req.URL.Query().Get("tailLines") != "" {
64-
logContent = "Line 4\nLine 5\n"
65-
}
66-
_, _ = w.Write([]byte(logContent))
67-
return
61+
_, err := strconv.Atoi(req.URL.Query().Get("tailLines"))
62+
if err == nil {
63+
logContent = "Line 4\nLine 5\n"
6864
}
65+
w.WriteHeader(http.StatusOK)
66+
_, _ = w.Write([]byte(logContent))
67+
return
6968
}
7069
w.WriteHeader(http.StatusNotFound)
7170
}))
@@ -83,9 +82,25 @@ func (s *NodesSuite) TestNodesLog() {
8382
"expected descriptive error '%s', got %v", expectedMessage, toolResult.Content[0].(mcp.TextContent).Text)
8483
})
8584
})
86-
s.Run("nodes_log(name=inexistent-node)", func() {
85+
s.Run("nodes_log(name=existing-node, query=nil)", func() {
86+
toolResult, err := s.CallTool("nodes_log", map[string]interface{}{
87+
"name": "existing-node",
88+
})
89+
s.Require().NotNil(toolResult, "toolResult should not be nil")
90+
s.Run("has error", func() {
91+
s.Truef(toolResult.IsError, "call tool should fail")
92+
s.Nilf(err, "call tool should not return error object")
93+
})
94+
s.Run("describes missing name", func() {
95+
expectedMessage := "failed to get node log, missing argument query"
96+
s.Equalf(expectedMessage, toolResult.Content[0].(mcp.TextContent).Text,
97+
"expected descriptive error '%s', got %v", expectedMessage, toolResult.Content[0].(mcp.TextContent).Text)
98+
})
99+
})
100+
s.Run("nodes_log(name=inexistent-node, query=/kubelet.log)", func() {
87101
toolResult, err := s.CallTool("nodes_log", map[string]interface{}{
88-
"name": "inexistent-node",
102+
"name": "inexistent-node",
103+
"query": "/kubelet.log",
89104
})
90105
s.Require().NotNil(toolResult, "toolResult should not be nil")
91106
s.Run("has error", func() {
@@ -98,10 +113,10 @@ func (s *NodesSuite) TestNodesLog() {
98113
"expected descriptive error '%s', got %v", expectedMessage, toolResult.Content[0].(mcp.TextContent).Text)
99114
})
100115
})
101-
s.Run("nodes_log(name=existing-node, log_path=missing.log)", func() {
116+
s.Run("nodes_log(name=existing-node, query=/missing.log)", func() {
102117
toolResult, err := s.CallTool("nodes_log", map[string]interface{}{
103-
"name": "existing-node",
104-
"log_path": "missing.log",
118+
"name": "existing-node",
119+
"query": "/missing.log",
105120
})
106121
s.Require().NotNil(toolResult, "toolResult should not be nil")
107122
s.Run("has error", func() {
@@ -114,10 +129,10 @@ func (s *NodesSuite) TestNodesLog() {
114129
"expected descriptive error '%s', got %v", expectedMessage, toolResult.Content[0].(mcp.TextContent).Text)
115130
})
116131
})
117-
s.Run("nodes_log(name=existing-node, log_path=empty.log)", func() {
132+
s.Run("nodes_log(name=existing-node, query=/empty.log)", func() {
118133
toolResult, err := s.CallTool("nodes_log", map[string]interface{}{
119-
"name": "existing-node",
120-
"log_path": "empty.log",
134+
"name": "existing-node",
135+
"query": "/empty.log",
121136
})
122137
s.Require().NotNil(toolResult, "toolResult should not be nil")
123138
s.Run("no error", func() {
@@ -130,10 +145,10 @@ func (s *NodesSuite) TestNodesLog() {
130145
"expected descriptive message '%s', got %v", expectedMessage, toolResult.Content[0].(mcp.TextContent).Text)
131146
})
132147
})
133-
s.Run("nodes_log(name=existing-node, log_path=kubelet.log)", func() {
148+
s.Run("nodes_log(name=existing-node, query=/kubelet.log)", func() {
134149
toolResult, err := s.CallTool("nodes_log", map[string]interface{}{
135-
"name": "existing-node",
136-
"log_path": "kubelet.log",
150+
"name": "existing-node",
151+
"query": "/kubelet.log",
137152
})
138153
s.Require().NotNil(toolResult, "toolResult should not be nil")
139154
s.Run("no error", func() {
@@ -147,11 +162,11 @@ func (s *NodesSuite) TestNodesLog() {
147162
})
148163
})
149164
for _, tailCase := range []interface{}{2, int64(2), float64(2)} {
150-
s.Run("nodes_log(name=existing-node, log_path=kubelet.log, tail=2)", func() {
165+
s.Run("nodes_log(name=existing-node, query=/kubelet.log, tailLines=2)", func() {
151166
toolResult, err := s.CallTool("nodes_log", map[string]interface{}{
152-
"name": "existing-node",
153-
"log_path": "kubelet.log",
154-
"tail": tailCase,
167+
"name": "existing-node",
168+
"query": "/kubelet.log",
169+
"tailLines": tailCase,
155170
})
156171
s.Require().NotNil(toolResult, "toolResult should not be nil")
157172
s.Run("no error", func() {
@@ -164,11 +179,11 @@ func (s *NodesSuite) TestNodesLog() {
164179
"expected log content '%s', got %v", expectedMessage, toolResult.Content[0].(mcp.TextContent).Text)
165180
})
166181
})
167-
s.Run("nodes_log(name=existing-node, log_path=kubelet.log, tail=-1)", func() {
182+
s.Run("nodes_log(name=existing-node, query=/kubelet.log, tailLines=-1)", func() {
168183
toolResult, err := s.CallTool("nodes_log", map[string]interface{}{
169-
"name": "existing-node",
170-
"log_path": "kubelet.log",
171-
"tail": -1,
184+
"name": "existing-node",
185+
"query": "/kubelet.log",
186+
"tail": -1,
172187
})
173188
s.Require().NotNil(toolResult, "toolResult should not be nil")
174189
s.Run("no error", func() {
@@ -191,7 +206,8 @@ func (s *NodesSuite) TestNodesLogDenied() {
191206
s.InitMcpClient()
192207
s.Run("nodes_log (denied)", func() {
193208
toolResult, err := s.CallTool("nodes_log", map[string]interface{}{
194-
"name": "does-not-matter",
209+
"name": "does-not-matter",
210+
"query": "/does-not-matter-either.log",
195211
})
196212
s.Require().NotNil(toolResult, "toolResult should not be nil")
197213
s.Run("has error", func() {

pkg/mcp/testdata/toolsets-core-tools.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,24 +45,24 @@
4545
"inputSchema": {
4646
"type": "object",
4747
"properties": {
48-
"log_path": {
49-
"default": "kubelet.log",
50-
"description": "Path to the log file on the node (e.g. 'kubelet.log', 'kube-proxy.log'). Default is 'kubelet.log'",
51-
"type": "string"
52-
},
5348
"name": {
5449
"description": "Name of the node to get logs from",
5550
"type": "string"
5651
},
57-
"tail": {
52+
"query": {
53+
"description": "query specifies services(s) or files from which to return logs (required). Example: \"kubelet\" to fetch kubelet logs, \"/\u003clog-file-name\u003e\" to fetch a specific log file from the node (e.g., \"/var/log/kubelet.log\" or \"/var/log/kube-proxy.log\")",
54+
"type": "string"
55+
},
56+
"tailLines": {
5857
"default": 100,
5958
"description": "Number of lines to retrieve from the end of the logs (Optional, 0 means all logs)",
6059
"minimum": 0,
6160
"type": "integer"
6261
}
6362
},
6463
"required": [
65-
"name"
64+
"name",
65+
"query"
6666
]
6767
},
6868
"name": "nodes_log"

pkg/mcp/testdata/toolsets-full-tools-multicluster-enum.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -215,24 +215,24 @@
215215
],
216216
"type": "string"
217217
},
218-
"log_path": {
219-
"default": "kubelet.log",
220-
"description": "Path to the log file on the node (e.g. 'kubelet.log', 'kube-proxy.log'). Default is 'kubelet.log'",
221-
"type": "string"
222-
},
223218
"name": {
224219
"description": "Name of the node to get logs from",
225220
"type": "string"
226221
},
227-
"tail": {
222+
"query": {
223+
"description": "query specifies services(s) or files from which to return logs (required). Example: \"kubelet\" to fetch kubelet logs, \"/\u003clog-file-name\u003e\" to fetch a specific log file from the node (e.g., \"/var/log/kubelet.log\" or \"/var/log/kube-proxy.log\")",
224+
"type": "string"
225+
},
226+
"tailLines": {
228227
"default": 100,
229228
"description": "Number of lines to retrieve from the end of the logs (Optional, 0 means all logs)",
230229
"minimum": 0,
231230
"type": "integer"
232231
}
233232
},
234233
"required": [
235-
"name"
234+
"name",
235+
"query"
236236
]
237237
},
238238
"name": "nodes_log"

pkg/mcp/testdata/toolsets-full-tools-multicluster.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -191,24 +191,24 @@
191191
"description": "Optional parameter selecting which context to run the tool in. Defaults to fake-context if not set",
192192
"type": "string"
193193
},
194-
"log_path": {
195-
"default": "kubelet.log",
196-
"description": "Path to the log file on the node (e.g. 'kubelet.log', 'kube-proxy.log'). Default is 'kubelet.log'",
197-
"type": "string"
198-
},
199194
"name": {
200195
"description": "Name of the node to get logs from",
201196
"type": "string"
202197
},
203-
"tail": {
198+
"query": {
199+
"description": "query specifies services(s) or files from which to return logs (required). Example: \"kubelet\" to fetch kubelet logs, \"/\u003clog-file-name\u003e\" to fetch a specific log file from the node (e.g., \"/var/log/kubelet.log\" or \"/var/log/kube-proxy.log\")",
200+
"type": "string"
201+
},
202+
"tailLines": {
204203
"default": 100,
205204
"description": "Number of lines to retrieve from the end of the logs (Optional, 0 means all logs)",
206205
"minimum": 0,
207206
"type": "integer"
208207
}
209208
},
210209
"required": [
211-
"name"
210+
"name",
211+
"query"
212212
]
213213
},
214214
"name": "nodes_log"

pkg/mcp/testdata/toolsets-full-tools-openshift.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -151,24 +151,24 @@
151151
"inputSchema": {
152152
"type": "object",
153153
"properties": {
154-
"log_path": {
155-
"default": "kubelet.log",
156-
"description": "Path to the log file on the node (e.g. 'kubelet.log', 'kube-proxy.log'). Default is 'kubelet.log'",
157-
"type": "string"
158-
},
159154
"name": {
160155
"description": "Name of the node to get logs from",
161156
"type": "string"
162157
},
163-
"tail": {
158+
"query": {
159+
"description": "query specifies services(s) or files from which to return logs (required). Example: \"kubelet\" to fetch kubelet logs, \"/\u003clog-file-name\u003e\" to fetch a specific log file from the node (e.g., \"/var/log/kubelet.log\" or \"/var/log/kube-proxy.log\")",
160+
"type": "string"
161+
},
162+
"tailLines": {
164163
"default": 100,
165164
"description": "Number of lines to retrieve from the end of the logs (Optional, 0 means all logs)",
166165
"minimum": 0,
167166
"type": "integer"
168167
}
169168
},
170169
"required": [
171-
"name"
170+
"name",
171+
"query"
172172
]
173173
},
174174
"name": "nodes_log"

pkg/mcp/testdata/toolsets-full-tools.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -151,24 +151,24 @@
151151
"inputSchema": {
152152
"type": "object",
153153
"properties": {
154-
"log_path": {
155-
"default": "kubelet.log",
156-
"description": "Path to the log file on the node (e.g. 'kubelet.log', 'kube-proxy.log'). Default is 'kubelet.log'",
157-
"type": "string"
158-
},
159154
"name": {
160155
"description": "Name of the node to get logs from",
161156
"type": "string"
162157
},
163-
"tail": {
158+
"query": {
159+
"description": "query specifies services(s) or files from which to return logs (required). Example: \"kubelet\" to fetch kubelet logs, \"/\u003clog-file-name\u003e\" to fetch a specific log file from the node (e.g., \"/var/log/kubelet.log\" or \"/var/log/kube-proxy.log\")",
160+
"type": "string"
161+
},
162+
"tailLines": {
164163
"default": 100,
165164
"description": "Number of lines to retrieve from the end of the logs (Optional, 0 means all logs)",
166165
"minimum": 0,
167166
"type": "integer"
168167
}
169168
},
170169
"required": [
171-
"name"
170+
"name",
171+
"query"
172172
]
173173
},
174174
"name": "nodes_log"

0 commit comments

Comments
 (0)