From 80bdc6550f72fa47f261271501bd2ea5b8d76856 Mon Sep 17 00:00:00 2001 From: Aditya Thebe Date: Wed, 20 May 2026 22:12:55 +0545 Subject: [PATCH 1/2] chore: Readme --- arthas/Plugin.yaml | 5 +-- arthas/README.md | 63 ++++++++++++++++++++++++++ inspektor-gadget/Plugin.yaml | 7 ++- inspektor-gadget/README.md | 86 ++++++++++++++++++++++++++++++++++++ 4 files changed, 154 insertions(+), 7 deletions(-) create mode 100644 arthas/README.md create mode 100644 inspektor-gadget/README.md diff --git a/arthas/Plugin.yaml b/arthas/Plugin.yaml index 9c5b88c..e290d2e 100644 --- a/arthas/Plugin.yaml +++ b/arthas/Plugin.yaml @@ -3,7 +3,7 @@ kind: Plugin metadata: name: arthas spec: - source: arthas + source: arthas-mc-plugin version: "0.1.0" selector: types: @@ -14,5 +14,4 @@ spec: - Kubernetes::ReplicaSet - Kubernetes::Job - Kubernetes::CronJob - connections: - kubernetes: {} + \ No newline at end of file diff --git a/arthas/README.md b/arthas/README.md new file mode 100644 index 0000000..077b281 --- /dev/null +++ b/arthas/README.md @@ -0,0 +1,63 @@ +# Arthas Mission Control Plugin + +The Arthas plugin adds JVM diagnostics to Mission Control for Kubernetes workloads. It attaches [Arthas](https://arthas.aliyun.com/) to a Java process running in a selected pod and exposes the Arthas web console, HTTP API, and diagnostic operations from the Mission Control UI. + +## What it does + +- Adds an **Arthas** tab to Kubernetes catalog items: + - `Kubernetes::Pod` + - `Kubernetes::Deployment` + - `Kubernetes::StatefulSet` + - `Kubernetes::DaemonSet` + - `Kubernetes::ReplicaSet` + - `Kubernetes::Job` + - `Kubernetes::CronJob` +- Resolves the selected workload to a running pod and container. +- Copies/installs `arthas-boot.jar` into the target pod when needed. +- Attaches Arthas to the JVM in the target container. +- Opens Kubernetes port-forwards to the Arthas HTTP console and optional MCP endpoint. +- Proxies the Arthas UI/API through Mission Control. +- Tracks active sessions inside the plugin process. + +## Operations + +| Operation | Purpose | +| ---------------- | --------------------------------------------------------------- | +| `sessions-list` | List active Arthas sessions. | +| `session-create` | Attach Arthas to the selected workload or pod. | +| `session-delete` | Stop and remove a plugin session and close port-forwards. | +| `pods-list` | List ready pods that can be targeted for the selected workload. | +| `exec` | Execute an Arthas command through the Arthas HTTP API. | + +## Kubernetes access + +The plugin needs Kubernetes API access to resolve pods, exec into containers, and create port-forwards. + +At runtime it first tries to resolve a Mission Control Kubernetes connection via: + +```go +host.GetConnectionByType(ctx, sdk.ConnectionTypeKubernetes) +``` + +The Plugin CRD must map that connection type, for example: + +```yaml +spec: + connections: + types: + kubernetes: connection://mission-control/kubernetes-vps-d1140a41 +``` + +If no usable host connection is available, it falls back to `rest.InClusterConfig()`, using the service account of the plugin process. + +## Configuration + +This plugin currently does not consume `spec.properties`; `Configure()` is a no-op. + +## Build + +From the repository root: + +```sh +task build:plugin:arthas +``` diff --git a/inspektor-gadget/Plugin.yaml b/inspektor-gadget/Plugin.yaml index 06afc74..a455aad 100644 --- a/inspektor-gadget/Plugin.yaml +++ b/inspektor-gadget/Plugin.yaml @@ -3,8 +3,8 @@ kind: Plugin metadata: name: inspektor-gadget spec: - source: inspektor-gadget - version: "0.1.0" + source: inspektor-gadget-mc-plugin + version: v1.0.2 selector: types: - Kubernetes::Pod @@ -14,5 +14,4 @@ spec: - Kubernetes::ReplicaSet - Kubernetes::Job - Kubernetes::CronJob - connections: - kubernetes: {} + \ No newline at end of file diff --git a/inspektor-gadget/README.md b/inspektor-gadget/README.md new file mode 100644 index 0000000..4c5d850 --- /dev/null +++ b/inspektor-gadget/README.md @@ -0,0 +1,86 @@ +# Inspektor Gadget Mission Control Plugin + +The Inspektor Gadget plugin adds eBPF-based Kubernetes workload diagnostics to Mission Control. +It can install/check the Inspektor Gadget deployment and run bounded gadget trace sessions against selected Kubernetes resources from the Mission Control UI. + +## What it does + +- Adds a **Gadget** tab to Kubernetes catalog items: + - `Kubernetes::Pod` + - `Kubernetes::Deployment` + - `Kubernetes::StatefulSet` + - `Kubernetes::DaemonSet` + - `Kubernetes::ReplicaSet` + - `Kubernetes::Job` + - `Kubernetes::CronJob` +- Checks whether Inspektor Gadget is deployed and ready. +- Can generate or apply the Inspektor Gadget Kubernetes manifest. +- Resolves the selected workload to pods, containers, nodes, or selectors. +- Starts eBPF gadget runs through the Inspektor Gadget gadget-service API. +- Uses Kubernetes API port-forwarding to reach gadget-service pods. +- Buffers trace events in the plugin process for UI/API retrieval. + +## Supported gadget categories + +The plugin exposes a curated list of Inspektor Gadget gadgets, including: + +- Runtime traces: exec, signals, OOM kills, malloc, deadlock, tty snoop +- Network traces: DNS, TCP, TCP drops/retransmits, SNI, SSL, bind, tcpdump +- File traces: open, fsnotify, fsslower, mount, file top/snapshot +- Security traces: seccomp audit/advice, Linux capabilities, LSM, module load +- Profiles and snapshots: CPU, block I/O, TCP RTT, process/socket/file snapshots +- Top-style gadgets: process, TCP, file, block I/O, CPU throttling, BPF stats + +## Operations + +| Operation | Purpose | +| -------------- | --------------------------------------------------------------- | +| `status` | Check Inspektor Gadget deployment readiness. | +| `install-plan` | Return the Kubernetes manifest that would be applied. | +| `install` | Apply the Inspektor Gadget manifest through the Kubernetes API. | +| `traces-list` | List supported gadgets. | +| `trace-start` | Start a bounded trace session for the selected resource. | +| `trace-stop` | Stop a running trace session. | +| `trace-list` | List active and recent trace sessions. | +| `trace-events` | Return buffered events for a trace session. | + +## Kubernetes access + +The plugin needs Kubernetes API access to inspect/install Inspektor Gadget, resolve workload pods, port-forward to gadget-service, and run traces. + +At runtime it first tries to resolve a Mission Control Kubernetes connection via: + +```go +host.GetConnectionByType(ctx, sdk.ConnectionTypeKubernetes) +``` + +The Plugin CRD must map that connection type, for example: + +```yaml +spec: + connections: + types: + kubernetes: connection://mission-control/kubernetes-vps-d1140a41 +``` + +If no usable host connection is available, it falls back to `rest.InClusterConfig()`, using the service account of the plugin process. + +## Configuration + +The plugin supports these `spec.properties` settings: + +| Property | Default | Description | +| ----------------- | --------- | -------------------------------------------------------------- | +| `gadgetNamespace` | `gadget` | Namespace where Inspektor Gadget is installed. | +| `gadgetTag` | `v0.52.0` | Inspektor Gadget image tag used for install and gadget images. | +| `maxDurationSec` | `900` | Maximum trace duration in seconds. | +| `maxEvents` | `10000` | Maximum buffered events per session. | +| `maxSessions` | `5` | Maximum concurrent trace sessions. | + +## Build + +From the repository root: + +```sh +task build:plugin:inspektor-gadget +``` From 0b1b6f8e9daae66e1a51d5e0279c10551b3ff5c1 Mon Sep 17 00:00:00 2001 From: Aditya Thebe Date: Wed, 20 May 2026 22:49:14 +0545 Subject: [PATCH 2/2] fix(plugins): expose UI operations over HTTP The plugin UI SDK calls operation proxy endpoints with POST bodies, but Arthas and Inspektor Gadget only registered gRPC handlers and did not declare POST HTTP bindings. Mission Control rejected those calls before they reached the plugin, causing forbidden errors such as traces-list not allowing HTTP POST. Declare POST bindings for unary UI operations and register HTTP adapters that invoke the existing handlers with the request body and host context. Include the updated Inspektor Gadget UI checksum. --- arthas/http.go | 30 ++++++++++++++++++++++++++++++ arthas/main.go | 11 +++++++++-- inspektor-gadget/http.go | 30 ++++++++++++++++++++++++++++++ inspektor-gadget/main.go | 11 +++++++++-- inspektor-gadget/ui/index.html | 2 +- inspektor-gadget/ui_checksum.go | 2 +- 6 files changed, 80 insertions(+), 6 deletions(-) diff --git a/arthas/http.go b/arthas/http.go index aaa1a6d..2081edc 100644 --- a/arthas/http.go +++ b/arthas/http.go @@ -2,6 +2,8 @@ package main import ( "bytes" + "context" + "encoding/json" "fmt" "io" "log" @@ -14,6 +16,34 @@ import ( "github.com/flanksource/incident-commander/plugin/sdk" ) +func (p *ArthasPlugin) httpInvoke(operation string, handler func(context.Context, sdk.InvokeCtx) (any, error)) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { _ = r.Body.Close() }() + params, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if len(strings.TrimSpace(string(params))) == 0 { + params = []byte("{}") + } + res, err := handler(r.Context(), sdk.InvokeCtx{ + Operation: operation, + ParamsJSON: params, + ConfigItemID: sdk.ConfigItemIDFromContext(r.Context()), + Host: sdk.HostClientFromContext(r.Context()), + }) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(res); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) +} + func (p *ArthasPlugin) HTTPHandler() http.Handler { mux := http.NewServeMux() mux.HandleFunc("/proxy/", p.httpProxyConsole) diff --git a/arthas/main.go b/arthas/main.go index 68281b6..a3e8dfd 100644 --- a/arthas/main.go +++ b/arthas/main.go @@ -9,6 +9,7 @@ import ( "context" "embed" "io/fs" + "net/http" pluginpb "github.com/flanksource/incident-commander/plugin/proto" "github.com/flanksource/incident-commander/plugin/sdk" @@ -79,7 +80,7 @@ func (p *ArthasPlugin) Operations() []sdk.Operation { out := make([]sdk.Operation, 0, len(defs)) for _, d := range defs { if h, ok := handlers[d.Name]; ok { - out = append(out, sdk.Operation{Def: d, Handler: h}) + out = append(out, sdk.Operation{Def: d, Handler: h, HTTPHandler: p.httpInvoke(d.Name, h)}) } } return out @@ -87,7 +88,13 @@ func (p *ArthasPlugin) Operations() []sdk.Operation { func operationDefs() []*pluginpb.OperationDef { mk := func(name, desc string) *pluginpb.OperationDef { - return &pluginpb.OperationDef{Name: name, Description: desc, Scope: "config", ResultMime: sdk.ClickyResultMimeType} + return &pluginpb.OperationDef{ + Name: name, + Description: desc, + Scope: "config", + ResultMime: sdk.ClickyResultMimeType, + Http: []*pluginpb.HTTPBinding{{Method: http.MethodPost}}, + } } return []*pluginpb.OperationDef{ mk(OpSessionsList, "List active Arthas sessions in this plugin process."), diff --git a/inspektor-gadget/http.go b/inspektor-gadget/http.go index 5d91f73..f26367f 100644 --- a/inspektor-gadget/http.go +++ b/inspektor-gadget/http.go @@ -1,14 +1,44 @@ package main import ( + "context" "encoding/json" "fmt" + "io" "net/http" "strings" "github.com/flanksource/incident-commander/plugin/sdk" ) +func (p *InspektorGadgetPlugin) httpInvoke(operation string, handler func(context.Context, sdk.InvokeCtx) (any, error)) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { _ = r.Body.Close() }() + params, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if len(strings.TrimSpace(string(params))) == 0 { + params = []byte("{}") + } + res, err := handler(r.Context(), sdk.InvokeCtx{ + Operation: operation, + ParamsJSON: params, + ConfigItemID: sdk.ConfigItemIDFromContext(r.Context()), + Host: sdk.HostClientFromContext(r.Context()), + }) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(res); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) +} + func (p *InspektorGadgetPlugin) HTTPHandler() http.Handler { mux := http.NewServeMux() mux.HandleFunc("/sessions/", p.httpSession) diff --git a/inspektor-gadget/main.go b/inspektor-gadget/main.go index 9c44207..5724807 100644 --- a/inspektor-gadget/main.go +++ b/inspektor-gadget/main.go @@ -9,6 +9,7 @@ import ( "context" "embed" "io/fs" + "net/http" pluginpb "github.com/flanksource/incident-commander/plugin/proto" "github.com/flanksource/incident-commander/plugin/sdk" @@ -130,7 +131,7 @@ func (p *InspektorGadgetPlugin) Operations() []sdk.Operation { out := make([]sdk.Operation, 0, len(defs)) for _, d := range defs { if h, ok := handlers[d.Name]; ok { - out = append(out, sdk.Operation{Def: d, Handler: h}) + out = append(out, sdk.Operation{Def: d, Handler: h, HTTPHandler: p.httpInvoke(d.Name, h)}) } } return out @@ -138,7 +139,13 @@ func (p *InspektorGadgetPlugin) Operations() []sdk.Operation { func operationDefs() []*pluginpb.OperationDef { mk := func(name, desc string) *pluginpb.OperationDef { - return &pluginpb.OperationDef{Name: name, Description: desc, Scope: "config", ResultMime: sdk.ClickyResultMimeType} + return &pluginpb.OperationDef{ + Name: name, + Description: desc, + Scope: "config", + ResultMime: sdk.ClickyResultMimeType, + Http: []*pluginpb.HTTPBinding{{Method: http.MethodPost}}, + } } return []*pluginpb.OperationDef{ mk(OpStatus, "Check Inspektor Gadget deployment readiness through the Kubernetes API."), diff --git a/inspektor-gadget/ui/index.html b/inspektor-gadget/ui/index.html index 4dc470b..4853bc8 100644 --- a/inspektor-gadget/ui/index.html +++ b/inspektor-gadget/ui/index.html @@ -4,7 +4,7 @@ Inspektor Gadget - + diff --git a/inspektor-gadget/ui_checksum.go b/inspektor-gadget/ui_checksum.go index e4b78b0..11df3f0 100644 --- a/inspektor-gadget/ui_checksum.go +++ b/inspektor-gadget/ui_checksum.go @@ -2,4 +2,4 @@ package main -const uiChecksum = "7cb4fe8476427a845b6519780265083367858adfd0d7a0a83e848db3ca636f32" +const uiChecksum = "f1c8ee07ec80cd019efd6e21eb171bc0ccb76f6cfed69a813b0819ef453304be"