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/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/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 +``` 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 @@