Skip to content

Commit 3e3d80f

Browse files
committed
Add run-aware logs for jobs and workflows
1 parent ce1ad74 commit 3e3d80f

19 files changed

Lines changed: 915 additions & 44 deletions

File tree

docs/mcp.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ Add to `~/.gemini/settings.json`:
194194
| `get_events` | Recent Kubernetes Warning events, deduplicated and sorted by recency. Filter by resource kind/name to scope. | `namespace` (optional), `limit` (optional, default 20, max 100), `kind` (optional), `name` (optional) |
195195
| `get_changes` | Recent meaningful changes from the Kubernetes cluster timeline plus native Helm release deployment/operation history (`source: helm`). Use to investigate what changed before an incident, including failed upgrades, rollbacks, and current Helm revisions. If the response includes `sourcesErrored`, treat it as partial data for those sources. Use `get_helm_release include=history,operations` for the full Helm revision trail. | `namespace` (optional), `kind` (optional), `name` (optional), `since` (optional, e.g. `1h`, `30m`; default `1h`), `limit` (optional, default 20, max 50) |
196196
| `get_pod_logs` | Filtered pod logs prioritizing errors/warnings, with secret redaction. Set `grep` for server-side filtering. | `namespace` (required), `name` (required), `container` (optional), `tail_lines` (optional, default 200), `grep` (optional) |
197-
| `get_workload_logs` | Aggregated, AI-filtered logs from all pods of a workload (Deployment, StatefulSet, DaemonSet) | `kind` (required), `namespace` (required), `name` (required), `container` (optional), `tail_lines` (optional, default 100 per pod), `grep` (optional) |
197+
| `get_workload_logs` | Aggregated, AI-filtered logs from all pods of a workload (Deployment, StatefulSet, DaemonSet, Job, Argo Workflow) | `kind` (required), `namespace` (required), `name` (required), `container` (optional), `tail_lines` (optional, default 100 per pod), `grep` (optional) |
198198
| `get_cluster_audit` | Static config posture — best-practice findings (Security / Reliability / Efficiency) with remediation. INDEPENDENT of operational health; for "what's broken right now?" use `issues`. | `namespace` (optional), `category` (optional), `severity` (optional) |
199199
| `list_packages` | Installed packages (Helm releases, label-managed workloads, CRDs, Argo Applications, Flux HelmReleases + Kustomizations) with source provenance, versions, and health, in one call. Response includes `sourceLegend` for the stable source codes. | `namespace` (optional), `source` (optional: `H`/`helm`, `L`/`labels`, `C`/`crds`, `A`/`argocd`, `F`/`fluxcd`), `chart` (optional substring) |
200200
| `list_helm_releases` | List Helm releases with status, resource health, storage namespace, Flux ownership, current `lastOperation`, and a capped `operations` trail when Helm history indicates failed upgrades, rollback-after-failure, rollbacks, or stuck pending operations. Use this first for Helm deployment debugging. | `namespace` (optional) |

internal/k8s/workload.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package k8s
22

33
import (
4+
"context"
45
"fmt"
56

67
corev1 "k8s.io/api/core/v1"
@@ -45,6 +46,28 @@ func GetWorkloadSelector(cache *ResourceCache, kind, namespace, name string) (*m
4546
}
4647
return ds.Spec.Selector, nil
4748

49+
case "job", "jobs":
50+
lister := cache.Jobs()
51+
if lister == nil {
52+
return nil, fmt.Errorf("insufficient permissions to list jobs")
53+
}
54+
job, err := lister.Jobs(namespace).Get(name)
55+
if err != nil {
56+
return nil, fmt.Errorf("job %s/%s not found: %w", namespace, name, err)
57+
}
58+
if job.Spec.Selector == nil {
59+
return nil, fmt.Errorf("job %s/%s has no pod selector", namespace, name)
60+
}
61+
return job.Spec.Selector, nil
62+
63+
case "workflow", "workflows":
64+
if _, err := cache.GetDynamicWithGroup(context.Background(), "Workflow", namespace, name, "argoproj.io"); err != nil {
65+
return nil, fmt.Errorf("workflow %s/%s not found: %w", namespace, name, err)
66+
}
67+
return &metav1.LabelSelector{
68+
MatchLabels: map[string]string{"workflows.argoproj.io/workflow": name},
69+
}, nil
70+
4871
default:
4972
return nil, fmt.Errorf("unsupported workload kind: %s", kind)
5073
}

internal/mcp/tools.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,7 @@ func registerTools(server *mcp.Server) {
450450
mcp.AddTool(server, &mcp.Tool{
451451
Name: "get_workload_logs",
452452
Description: "Get aggregated logs from all pods of a workload (Deployment, StatefulSet, " +
453-
"or DaemonSet). Logs are collected from all matching pods concurrently, then " +
453+
"DaemonSet, Job, or Argo Workflow). Logs are collected from all matching pods concurrently, then " +
454454
"server-side filtered to errors, warnings, panics, and stack traces using " +
455455
"deterministic regex patterns and deduplicated. Set grep for additional " +
456456
"server-side filtering before that summary stage, like `kubectl logs | grep PATTERN`. " +

internal/mcp/tools_filter_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,12 @@ func TestNormalizeWorkloadLogsKind_DefaultsToDeployment(t *testing.T) {
699699
if got := normalizeWorkloadLogsKind("statefulset"); got != "statefulsets" {
700700
t.Fatalf("statefulset workload-log kind = %q, want statefulsets", got)
701701
}
702+
if got := normalizeWorkloadLogsKind("job"); got != "jobs" {
703+
t.Fatalf("job workload-log kind = %q, want jobs", got)
704+
}
705+
if got := normalizeWorkloadLogsKind("Workflow"); got != "workflows" {
706+
t.Fatalf("Workflow workload-log kind = %q, want workflows", got)
707+
}
702708
}
703709

704710
func TestParseLogsSince(t *testing.T) {

internal/mcp/tools_workloads.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ type manageCronJobInput struct {
3636
}
3737

3838
type getWorkloadLogsInput struct {
39-
Kind string `json:"kind,omitempty" jsonschema:"workload kind: deployment, statefulset, or daemonset. Defaults to deployment when omitted."`
39+
Kind string `json:"kind,omitempty" jsonschema:"workload kind: deployment, statefulset, daemonset, job, or workflow. Defaults to deployment when omitted."`
4040
Namespace string `json:"namespace" jsonschema:"workload namespace"`
4141
Name string `json:"name" jsonschema:"workload name"`
4242
Container string `json:"container,omitempty" jsonschema:"specific container name, defaults to all containers"`
@@ -175,7 +175,7 @@ func handleManageCronJob(ctx context.Context, req *mcp.CallToolRequest, input ma
175175
func handleGetWorkloadLogs(ctx context.Context, req *mcp.CallToolRequest, input getWorkloadLogsInput) (*mcp.CallToolResult, any, error) {
176176
kind := normalizeWorkloadLogsKind(input.Kind)
177177
if kind == "" {
178-
return nil, nil, fmt.Errorf("invalid kind %q: must be deployment, statefulset, or daemonset", input.Kind)
178+
return nil, nil, fmt.Errorf("invalid kind %q: must be deployment, statefulset, daemonset, job, or workflow", input.Kind)
179179
}
180180

181181
if !checkNamespaceAccess(ctx, input.Namespace) {
@@ -568,5 +568,12 @@ func normalizeWorkloadLogsKind(kind string) string {
568568
if strings.TrimSpace(kind) == "" {
569569
return "deployments"
570570
}
571-
return normalizeWorkloadKind(kind)
571+
switch strings.ToLower(kind) {
572+
case "job", "jobs":
573+
return "jobs"
574+
case "workflow", "workflows":
575+
return "workflows"
576+
default:
577+
return normalizeWorkloadKind(kind)
578+
}
572579
}

internal/server/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,7 @@ func (s *Server) setupRoutes() {
406406

407407
// Workload logs (non-streaming)
408408
r.Get("/workloads/{kind}/{namespace}/{name}/logs", s.handleWorkloadLogs)
409+
r.Get("/workloads/{kind}/{namespace}/{name}/runs", s.handleWorkloadRuns)
409410
r.Get("/workloads/{kind}/{namespace}/{name}/pods", s.handleWorkloadPods)
410411

411412
// Helm routes

0 commit comments

Comments
 (0)