Skip to content

Commit 91dfba0

Browse files
authored
feat: add kubernetes_list_cronjobs tool (#35)
1 parent a258bf4 commit 91dfba0

File tree

5 files changed

+231
-0
lines changed

5 files changed

+231
-0
lines changed

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ The handler filters tools dynamically based on `GetMyPermissions` from Sysdig Se
5353
| `kubernetes_list_nodes` | `tool_kubernetes_list_nodes.go` | Lists Kubernetes node information. | `promql.exec` | "List all Kubernetes nodes in the cluster 'production-gke'" |
5454
| `kubernetes_list_workloads` | `tool_kubernetes_list_workloads.go` | Lists Kubernetes workload information. | `promql.exec` | "List all desired workloads in the cluster 'production-gke' and namespace 'default'" |
5555
| `kubernetes_list_pod_containers` | `tool_kubernetes_list_pod_containers.go` | Retrieves information from a particular pod and container. | `promql.exec` | "Show me info for pod 'my-pod' in cluster 'production-gke'" |
56+
| `kubernetes_list_cronjobs` | `tool_kubernetes_list_cronjobs.go` | Retrieves information from the cronjobs in the cluster. | `promql.exec` | "List all cronjobs in cluster 'prod' and namespace 'default'" |
5657

5758
Every tool has a companion `_test.go` file that exercises request validation, permission metadata, and Sysdig client calls through mocks.
5859
Note that if you add more tools you need to also update this file to reflect that.

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,11 @@ The server dynamically filters the available tools based on the permissions asso
128128
- **Required Permission**: `promql.exec`
129129
- **Sample Prompt**: "Show me info for pod 'my-pod' in cluster 'production-gke'"
130130

131+
- **`kubernetes_list_cronjobs`**
132+
- **Description**: Retrieves information from the cronjobs in the cluster.
133+
- **Required Permission**: `promql.exec`
134+
- **Sample Prompt**: "List all cronjobs in cluster 'prod' and namespace 'default'"
135+
131136
## Requirements
132137

133138
- [Go](https://go.dev/doc/install) 1.25 or higher (if running without Docker).

cmd/server/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ func setupHandler(sysdigClient sysdig.ExtendedClientWithResponsesInterface) *mcp
9797

9898
tools.NewKubernetesListClusters(sysdigClient),
9999
tools.NewKubernetesListNodes(sysdigClient),
100+
tools.NewKubernetesListCronjobs(sysdigClient),
100101
tools.NewKubernetesListWorkloads(sysdigClient),
101102
tools.NewKubernetesListPodContainers(sysdigClient),
102103
)
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package tools
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"strings"
9+
10+
"github.com/mark3labs/mcp-go/mcp"
11+
"github.com/mark3labs/mcp-go/server"
12+
"github.com/sysdiglabs/sysdig-mcp-server/internal/infra/sysdig"
13+
)
14+
15+
type KubernetesListCronjobs struct {
16+
SysdigClient sysdig.ExtendedClientWithResponsesInterface
17+
}
18+
19+
func NewKubernetesListCronjobs(sysdigClient sysdig.ExtendedClientWithResponsesInterface) *KubernetesListCronjobs {
20+
return &KubernetesListCronjobs{
21+
SysdigClient: sysdigClient,
22+
}
23+
}
24+
25+
func (t *KubernetesListCronjobs) RegisterInServer(s *server.MCPServer) {
26+
tool := mcp.NewTool("kubernetes_list_cronjobs",
27+
mcp.WithDescription("Retrieves information from the cronjobs in the cluster."),
28+
mcp.WithString("cluster_name", mcp.Description("The name of the cluster to filter by.")),
29+
mcp.WithString("namespace_name", mcp.Description("The name of the namespace to filter by.")),
30+
mcp.WithString("cronjob_name", mcp.Description("The name of the cronjob to filter by.")),
31+
mcp.WithNumber("limit",
32+
mcp.Description("Maximum number of cronjobs to return."),
33+
mcp.DefaultNumber(10),
34+
),
35+
mcp.WithOutputSchema[map[string]any](),
36+
WithRequiredPermissions(), // FIXME(fede): Add the required permissions. It should be `promql.exec` but somehow the token does not have that permission even if you are able to execute queries.
37+
)
38+
s.AddTool(tool, t.handle)
39+
}
40+
41+
func (t *KubernetesListCronjobs) handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
42+
clusterName := mcp.ParseString(request, "cluster_name", "")
43+
namespaceName := mcp.ParseString(request, "namespace_name", "")
44+
cronjobName := mcp.ParseString(request, "cronjob_name", "")
45+
limit := mcp.ParseInt(request, "limit", 10)
46+
47+
query := buildKubeCronjobInfoQuery(clusterName, namespaceName, cronjobName)
48+
49+
limitQuery := sysdig.LimitQuery(limit)
50+
params := &sysdig.GetQueryV1Params{
51+
Query: query,
52+
Limit: &limitQuery,
53+
}
54+
55+
httpResp, err := t.SysdigClient.GetQueryV1(ctx, params)
56+
if err != nil {
57+
return mcp.NewToolResultErrorFromErr("failed to get cronjob list", err), nil
58+
}
59+
60+
if httpResp.StatusCode != 200 {
61+
bodyBytes, _ := io.ReadAll(httpResp.Body)
62+
return mcp.NewToolResultErrorf("failed to get cronjob list: status code %d, body: %s", httpResp.StatusCode, string(bodyBytes)), nil
63+
}
64+
65+
var queryResponse sysdig.QueryResponseV1
66+
if err := json.NewDecoder(httpResp.Body).Decode(&queryResponse); err != nil {
67+
return mcp.NewToolResultErrorFromErr("failed to decode response", err), nil
68+
}
69+
70+
return mcp.NewToolResultJSON(queryResponse)
71+
}
72+
73+
func buildKubeCronjobInfoQuery(clusterName, namespaceName, cronjobName string) string {
74+
filters := []string{}
75+
if clusterName != "" {
76+
filters = append(filters, fmt.Sprintf("kube_cluster_name=\"%s\"", clusterName))
77+
}
78+
if namespaceName != "" {
79+
filters = append(filters, fmt.Sprintf("kube_namespace_name=\"%s\"", namespaceName))
80+
}
81+
if cronjobName != "" {
82+
filters = append(filters, fmt.Sprintf("kube_cronjob_name=\"%s\"", cronjobName))
83+
}
84+
85+
if len(filters) == 0 {
86+
return "kube_cronjob_info"
87+
}
88+
89+
return fmt.Sprintf("kube_cronjob_info{%s}", strings.Join(filters, ","))
90+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package tools_test
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"io"
7+
"net/http"
8+
9+
"github.com/mark3labs/mcp-go/mcp"
10+
"github.com/mark3labs/mcp-go/server"
11+
. "github.com/onsi/ginkgo/v2"
12+
. "github.com/onsi/gomega"
13+
"github.com/sysdiglabs/sysdig-mcp-server/internal/infra/mcp/tools"
14+
"github.com/sysdiglabs/sysdig-mcp-server/internal/infra/sysdig"
15+
"github.com/sysdiglabs/sysdig-mcp-server/internal/infra/sysdig/mocks"
16+
"go.uber.org/mock/gomock"
17+
)
18+
19+
var _ = Describe("KubernetesListCronjobs Tool", func() {
20+
var (
21+
tool *tools.KubernetesListCronjobs
22+
mockSysdig *mocks.MockExtendedClientWithResponsesInterface
23+
mcpServer *server.MCPServer
24+
ctrl *gomock.Controller
25+
)
26+
27+
BeforeEach(func() {
28+
ctrl = gomock.NewController(GinkgoT())
29+
mockSysdig = mocks.NewMockExtendedClientWithResponsesInterface(ctrl)
30+
tool = tools.NewKubernetesListCronjobs(mockSysdig)
31+
mcpServer = server.NewMCPServer("test", "test")
32+
tool.RegisterInServer(mcpServer)
33+
})
34+
35+
It("should register successfully in the server", func() {
36+
Expect(mcpServer.GetTool("kubernetes_list_cronjobs")).NotTo(BeNil())
37+
})
38+
39+
When("listing cronjobs", func() {
40+
DescribeTable("it succeeds", func(ctx context.Context, toolName string, request mcp.CallToolRequest, expectedParamsRequested sysdig.GetQueryV1Params) {
41+
mockSysdig.EXPECT().GetQueryV1(gomock.Any(), &expectedParamsRequested).Return(&http.Response{
42+
StatusCode: http.StatusOK,
43+
Body: io.NopCloser(bytes.NewBufferString(`{"status":"success"}`)),
44+
}, nil)
45+
46+
serverTool := mcpServer.GetTool(toolName)
47+
result, err := serverTool.Handler(ctx, request)
48+
Expect(err).NotTo(HaveOccurred())
49+
50+
resultData, ok := result.Content[0].(mcp.TextContent)
51+
Expect(ok).To(BeTrue())
52+
Expect(resultData.Text).To(MatchJSON(`{"status":"success"}`))
53+
},
54+
Entry(nil,
55+
"kubernetes_list_cronjobs",
56+
mcp.CallToolRequest{
57+
Params: mcp.CallToolParams{
58+
Name: "kubernetes_list_cronjobs",
59+
Arguments: map[string]any{},
60+
},
61+
},
62+
sysdig.GetQueryV1Params{
63+
Query: `kube_cronjob_info`,
64+
Limit: asPtr(sysdig.LimitQuery(10)),
65+
},
66+
),
67+
Entry(nil,
68+
"kubernetes_list_cronjobs",
69+
mcp.CallToolRequest{
70+
Params: mcp.CallToolParams{
71+
Name: "kubernetes_list_cronjobs",
72+
Arguments: map[string]any{"limit": "20"},
73+
},
74+
},
75+
sysdig.GetQueryV1Params{
76+
Query: `kube_cronjob_info`,
77+
Limit: asPtr(sysdig.LimitQuery(20)),
78+
},
79+
),
80+
Entry(nil,
81+
"kubernetes_list_cronjobs",
82+
mcp.CallToolRequest{
83+
Params: mcp.CallToolParams{
84+
Name: "kubernetes_list_cronjobs",
85+
Arguments: map[string]any{"cluster_name": "my_cluster"},
86+
},
87+
},
88+
sysdig.GetQueryV1Params{
89+
Query: `kube_cronjob_info{kube_cluster_name="my_cluster"}`,
90+
Limit: asPtr(sysdig.LimitQuery(10)),
91+
},
92+
),
93+
Entry(nil,
94+
"kubernetes_list_cronjobs",
95+
mcp.CallToolRequest{
96+
Params: mcp.CallToolParams{
97+
Name: "kubernetes_list_cronjobs",
98+
Arguments: map[string]any{"namespace_name": "my_namespace"},
99+
},
100+
},
101+
sysdig.GetQueryV1Params{
102+
Query: `kube_cronjob_info{kube_namespace_name="my_namespace"}`,
103+
Limit: asPtr(sysdig.LimitQuery(10)),
104+
},
105+
),
106+
Entry(nil,
107+
"kubernetes_list_cronjobs",
108+
mcp.CallToolRequest{
109+
Params: mcp.CallToolParams{
110+
Name: "kubernetes_list_cronjobs",
111+
Arguments: map[string]any{"cronjob_name": "my_cronjob"},
112+
},
113+
},
114+
sysdig.GetQueryV1Params{
115+
Query: `kube_cronjob_info{kube_cronjob_name="my_cronjob"}`,
116+
Limit: asPtr(sysdig.LimitQuery(10)),
117+
},
118+
),
119+
Entry(nil,
120+
"kubernetes_list_cronjobs",
121+
mcp.CallToolRequest{
122+
Params: mcp.CallToolParams{
123+
Name: "kubernetes_list_cronjobs",
124+
Arguments: map[string]any{"cluster_name": "my_cluster", "namespace_name": "my_namespace", "cronjob_name": "my_cronjob"},
125+
},
126+
},
127+
sysdig.GetQueryV1Params{
128+
Query: `kube_cronjob_info{kube_cluster_name="my_cluster",kube_namespace_name="my_namespace",kube_cronjob_name="my_cronjob"}`,
129+
Limit: asPtr(sysdig.LimitQuery(10)),
130+
},
131+
),
132+
)
133+
})
134+
})

0 commit comments

Comments
 (0)