Skip to content

Commit 44053f1

Browse files
fix(nodes): nodes_log query and tailLines arguments (#409)
* Fix the node log query Signed-off-by: Neeraj Krishna Gopalakrishna <[email protected]> * Fix the node log query Signed-off-by: Neeraj Krishna Gopalakrishna <[email protected]> * fix(nodes): nodes_log query and tailLines arguments Signed-off-by: Marc Nuri <[email protected]> --------- Signed-off-by: Neeraj Krishna Gopalakrishna <[email protected]> Signed-off-by: Marc Nuri <[email protected]> Co-authored-by: Marc Nuri <[email protected]>
1 parent c001997 commit 44053f1

File tree

10 files changed

+114
-86
lines changed

10 files changed

+114
-86
lines changed

README.md

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

245245
- **projects_list** - List all the OpenShift projects in the current cluster
246246

247+
- **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
248+
- `name` (`string`) **(required)** - Name of the node to get logs from
249+
- `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")
250+
- `tailLines` (`integer`) - Number of lines to retrieve from the end of the logs (Optional, 0 means all logs)
251+
247252
- **pods_list** - List all the Kubernetes pods in the current cluster from all namespaces
248253
- `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
249254

pkg/kubernetes/accesscontrol_clientset.go

Lines changed: 2 additions & 2 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)
@@ -49,7 +49,7 @@ func (a *AccessControlClientset) NodesLogs(ctx context.Context, name, logPath st
4949
return nil, fmt.Errorf("failed to get node %s: %w", name, err)
5050
}
5151

52-
url := []string{"api", "v1", "nodes", name, "proxy", "logs", logPath}
52+
url := []string{"api", "v1", "nodes", name, "proxy", "logs"}
5353
return a.delegate.CoreV1().RESTClient().
5454
Get().
5555
AbsPath(url...), nil

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: 54 additions & 32 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,21 +44,25 @@ func (s *NodesSuite) TestNodesLog() {
4344
}`))
4445
return
4546
}
46-
// Get Empty Log response
47-
if req.URL.Path == "/api/v1/nodes/existing-node/proxy/logs/empty.log" {
47+
// Get Proxy Logs
48+
if req.URL.Path == "/api/v1/nodes/existing-node/proxy/logs" {
4849
w.Header().Set("Content-Type", "text/plain")
49-
w.WriteHeader(http.StatusOK)
50-
_, _ = w.Write([]byte(``))
51-
return
52-
}
53-
// Get Kubelet Log response
54-
if req.URL.Path == "/api/v1/nodes/existing-node/proxy/logs/kubelet.log" {
55-
w.Header().Set("Content-Type", "text/plain")
56-
w.WriteHeader(http.StatusOK)
57-
logContent := "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\n"
58-
if req.URL.Query().Get("tailLines") != "" {
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)
59+
return
60+
}
61+
_, err := strconv.Atoi(req.URL.Query().Get("tailLines"))
62+
if err == nil {
5963
logContent = "Line 4\nLine 5\n"
6064
}
65+
w.WriteHeader(http.StatusOK)
6166
_, _ = w.Write([]byte(logContent))
6267
return
6368
}
@@ -77,9 +82,25 @@ func (s *NodesSuite) TestNodesLog() {
7782
"expected descriptive error '%s', got %v", expectedMessage, toolResult.Content[0].(mcp.TextContent).Text)
7883
})
7984
})
80-
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() {
81101
toolResult, err := s.CallTool("nodes_log", map[string]interface{}{
82-
"name": "inexistent-node",
102+
"name": "inexistent-node",
103+
"query": "/kubelet.log",
83104
})
84105
s.Require().NotNil(toolResult, "toolResult should not be nil")
85106
s.Run("has error", func() {
@@ -92,10 +113,10 @@ func (s *NodesSuite) TestNodesLog() {
92113
"expected descriptive error '%s', got %v", expectedMessage, toolResult.Content[0].(mcp.TextContent).Text)
93114
})
94115
})
95-
s.Run("nodes_log(name=existing-node, log_path=missing.log)", func() {
116+
s.Run("nodes_log(name=existing-node, query=/missing.log)", func() {
96117
toolResult, err := s.CallTool("nodes_log", map[string]interface{}{
97-
"name": "existing-node",
98-
"log_path": "missing.log",
118+
"name": "existing-node",
119+
"query": "/missing.log",
99120
})
100121
s.Require().NotNil(toolResult, "toolResult should not be nil")
101122
s.Run("has error", func() {
@@ -108,10 +129,10 @@ func (s *NodesSuite) TestNodesLog() {
108129
"expected descriptive error '%s', got %v", expectedMessage, toolResult.Content[0].(mcp.TextContent).Text)
109130
})
110131
})
111-
s.Run("nodes_log(name=existing-node, log_path=empty.log)", func() {
132+
s.Run("nodes_log(name=existing-node, query=/empty.log)", func() {
112133
toolResult, err := s.CallTool("nodes_log", map[string]interface{}{
113-
"name": "existing-node",
114-
"log_path": "empty.log",
134+
"name": "existing-node",
135+
"query": "/empty.log",
115136
})
116137
s.Require().NotNil(toolResult, "toolResult should not be nil")
117138
s.Run("no error", func() {
@@ -124,10 +145,10 @@ func (s *NodesSuite) TestNodesLog() {
124145
"expected descriptive message '%s', got %v", expectedMessage, toolResult.Content[0].(mcp.TextContent).Text)
125146
})
126147
})
127-
s.Run("nodes_log(name=existing-node, log_path=kubelet.log)", func() {
148+
s.Run("nodes_log(name=existing-node, query=/kubelet.log)", func() {
128149
toolResult, err := s.CallTool("nodes_log", map[string]interface{}{
129-
"name": "existing-node",
130-
"log_path": "kubelet.log",
150+
"name": "existing-node",
151+
"query": "/kubelet.log",
131152
})
132153
s.Require().NotNil(toolResult, "toolResult should not be nil")
133154
s.Run("no error", func() {
@@ -141,11 +162,11 @@ func (s *NodesSuite) TestNodesLog() {
141162
})
142163
})
143164
for _, tailCase := range []interface{}{2, int64(2), float64(2)} {
144-
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() {
145166
toolResult, err := s.CallTool("nodes_log", map[string]interface{}{
146-
"name": "existing-node",
147-
"log_path": "kubelet.log",
148-
"tail": tailCase,
167+
"name": "existing-node",
168+
"query": "/kubelet.log",
169+
"tailLines": tailCase,
149170
})
150171
s.Require().NotNil(toolResult, "toolResult should not be nil")
151172
s.Run("no error", func() {
@@ -158,11 +179,11 @@ func (s *NodesSuite) TestNodesLog() {
158179
"expected log content '%s', got %v", expectedMessage, toolResult.Content[0].(mcp.TextContent).Text)
159180
})
160181
})
161-
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() {
162183
toolResult, err := s.CallTool("nodes_log", map[string]interface{}{
163-
"name": "existing-node",
164-
"log_path": "kubelet.log",
165-
"tail": -1,
184+
"name": "existing-node",
185+
"query": "/kubelet.log",
186+
"tail": -1,
166187
})
167188
s.Require().NotNil(toolResult, "toolResult should not be nil")
168189
s.Run("no error", func() {
@@ -185,7 +206,8 @@ func (s *NodesSuite) TestNodesLogDenied() {
185206
s.InitMcpClient()
186207
s.Run("nodes_log (denied)", func() {
187208
toolResult, err := s.CallTool("nodes_log", map[string]interface{}{
188-
"name": "does-not-matter",
209+
"name": "does-not-matter",
210+
"query": "/does-not-matter-either.log",
189211
})
190212
s.Require().NotNil(toolResult, "toolResult should not be nil")
191213
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)