feat(pods): add optional tail parameter to pod logs retrieval (#335)

* feat(pods): add tailLines parameter to pod logs retrieval with default 256 lines

Signed-off-by: iamsudip <sudip.maji@harness.io>

* address review comments

Signed-off-by: iamsudip <sudip.maji@harness.io>

* test(pods): add tailLines parameter to pod logs retrieval with default 256 lines

Signed-off-by: Marc Nuri <marc@marcnuri.com>

---------

Signed-off-by: iamsudip <sudip.maji@harness.io>
Signed-off-by: Marc Nuri <marc@marcnuri.com>
Co-authored-by: Marc Nuri <marc@marcnuri.com>
This commit is contained in:
iamsudip
2025-09-25 12:47:53 +05:30
committed by GitHub
parent 8e666d4c67
commit 053fb2e31c
7 changed files with 96 additions and 6 deletions

View File

@@ -261,6 +261,7 @@ The following sets of tools are available (all on by default):
- `name` (`string`) **(required)** - Name of the Pod to get the logs from
- `namespace` (`string`) - Namespace to get the Pod logs from
- `previous` (`boolean`) - Return previous terminated container logs (Optional)
- `tail` (`number`) - Number of lines to retrieve from the end of the logs (Optional, default: 100)
- **pods_run** - Run a Kubernetes Pod in the current or provided namespace with the provided container image and optional name
- `image` (`string`) **(required)** - Container Image to run in the Pod

View File

@@ -17,10 +17,14 @@ import (
"k8s.io/client-go/tools/remotecommand"
"k8s.io/metrics/pkg/apis/metrics"
metricsv1beta1api "k8s.io/metrics/pkg/apis/metrics/v1beta1"
"k8s.io/utils/ptr"
"github.com/containers/kubernetes-mcp-server/pkg/version"
)
// Default number of lines to retrieve from the end of the logs
const DefaultTailLines = int64(100)
type PodsTopOptions struct {
metav1.ListOptions
AllNamespaces bool
@@ -92,17 +96,26 @@ func (k *Kubernetes) PodsDelete(ctx context.Context, namespace, name string) (st
k.ResourcesDelete(ctx, &schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}, namespace, name)
}
func (k *Kubernetes) PodsLog(ctx context.Context, namespace, name, container string, previous bool) (string, error) {
tailLines := int64(256)
func (k *Kubernetes) PodsLog(ctx context.Context, namespace, name, container string, previous bool, tail int64) (string, error) {
pods, err := k.manager.accessControlClientSet.Pods(k.NamespaceOrDefault(namespace))
if err != nil {
return "", err
}
req := pods.GetLogs(name, &v1.PodLogOptions{
TailLines: &tailLines,
logOptions := &v1.PodLogOptions{
Container: container,
Previous: previous,
})
}
// Only set tailLines if a value is provided (non-zero)
if tail > 0 {
logOptions.TailLines = &tail
} else {
// Default to DefaultTailLines lines when not specified
logOptions.TailLines = ptr.To(DefaultTailLines)
}
req := pods.GetLogs(name, logOptions)
res := req.Do(ctx)
if res.Error() != nil {
return "", res.Error()

View File

@@ -756,6 +756,41 @@ func TestPodsLog(t *testing.T) {
return
}
})
// Test with tail parameter
podsTailLines, err := c.callTool("pods_log", map[string]interface{}{
"namespace": "ns-1",
"name": "a-pod-in-ns-1",
"tail": 50,
})
t.Run("pods_log with tail=50 returns pod log", func(t *testing.T) {
if err != nil {
t.Fatalf("call tool failed %v", err)
return
}
if podsTailLines.IsError {
t.Fatalf("call tool failed")
return
}
})
// Test with invalid tail parameter
podsInvalidTailLines, _ := c.callTool("pods_log", map[string]interface{}{
"namespace": "ns-1",
"name": "a-pod-in-ns-1",
"tail": "invalid",
})
t.Run("pods_log with invalid tail returns error", func(t *testing.T) {
if !podsInvalidTailLines.IsError {
t.Fatalf("call tool should fail")
return
}
expectedErrorMsg := "failed to parse tail parameter: expected integer"
if errMsg := podsInvalidTailLines.Content[0].(mcp.TextContent).Text; !strings.Contains(errMsg, expectedErrorMsg) {
t.Fatalf("unexpected error message, expected to contain '%s', got '%s'", expectedErrorMsg, errMsg)
return
}
})
})
}

View File

@@ -202,6 +202,12 @@
"previous": {
"description": "Return previous terminated container logs (Optional)",
"type": "boolean"
},
"tail": {
"default": 100,
"description": "Number of lines to retrieve from the end of the logs (Optional, default: 100)",
"minimum": 0,
"type": "integer"
}
},
"required": [

View File

@@ -308,6 +308,12 @@
"previous": {
"description": "Return previous terminated container logs (Optional)",
"type": "boolean"
},
"tail": {
"default": 100,
"description": "Number of lines to retrieve from the end of the logs (Optional, default: 100)",
"minimum": 0,
"type": "integer"
}
},
"required": [

View File

@@ -308,6 +308,12 @@
"previous": {
"description": "Return previous terminated container logs (Optional)",
"type": "boolean"
},
"tail": {
"default": 100,
"description": "Number of lines to retrieve from the end of the logs (Optional, default: 100)",
"minimum": 0,
"type": "integer"
}
},
"required": [

View File

@@ -201,6 +201,12 @@ func initPods() []api.ServerTool {
Type: "string",
Description: "Name of the Pod container to get the logs from (Optional)",
},
"tail": {
Type: "integer",
Description: "Number of lines to retrieve from the end of the logs (Optional, default: 100)",
Default: api.ToRawMessage(kubernetes.DefaultTailLines),
Minimum: ptr.To(float64(0)),
},
"previous": {
Type: "boolean",
Description: "Return previous terminated container logs (Optional)",
@@ -396,7 +402,24 @@ func podsLog(params api.ToolHandlerParams) (*api.ToolCallResult, error) {
if previous != nil {
previousBool = previous.(bool)
}
ret, err := params.PodsLog(params, ns.(string), name.(string), container.(string), previousBool)
// Extract tailLines parameter
tail := params.GetArguments()["tail"]
var tailInt int64
if tail != nil {
// Convert to int64 - safely handle both float64 (JSON number) and int types
switch v := tail.(type) {
case float64:
tailInt = int64(v)
case int:
tailInt = int64(v)
case int64:
tailInt = v
default:
return api.NewToolCallResult("", fmt.Errorf("failed to parse tail parameter: expected integer, got %T", tail)), nil
}
}
ret, err := params.PodsLog(params.Context, ns.(string), name.(string), container.(string), previousBool, tailInt)
if err != nil {
return api.NewToolCallResult("", fmt.Errorf("failed to get pod %s log in namespace %s: %v", name, ns, err)), nil
} else if ret == "" {