Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 63 additions & 3 deletions arthas/ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,18 @@ type ExecResponse struct {
Duration time.Duration `json:"-"`
}

func (p *ArthasPlugin) sessionsList(_ context.Context, _ sdk.InvokeCtx) (any, error) {
return p.sessions.List(), nil
func (p *ArthasPlugin) sessionsList(ctx context.Context, req sdk.InvokeCtx) (any, error) {
pods, err := p.currentPods(ctx, req)
if err != nil {
return nil, err
}
out := make([]*arthas.Session, 0)
for _, sess := range p.sessions.List() {
if sessionInPods(sess.Namespace, sess.Pod, sess.Container, pods) {
out = append(out, sess)
}
}
return out, nil
}

func (p *ArthasPlugin) podsList(ctx context.Context, req sdk.InvokeCtx) (any, error) {
Expand Down Expand Up @@ -251,14 +261,17 @@ func (p *ArthasPlugin) createTarget(ctx context.Context, req sdk.InvokeCtx, para
return base, nil
}

func (p *ArthasPlugin) sessionDelete(_ context.Context, req sdk.InvokeCtx) (any, error) {
func (p *ArthasPlugin) sessionDelete(ctx context.Context, req sdk.InvokeCtx) (any, error) {
var params SessionDeleteParams
if err := json.Unmarshal(req.ParamsJSON, &params); err != nil {
return nil, fmt.Errorf("decode params: %w", err)
}
if params.ID == "" {
return nil, fmt.Errorf("id is required")
}
if _, err := p.sessionForConfig(ctx, req, params.ID); err != nil {
return nil, err
}
removed, err := p.sessions.Remove(params.ID)
if !removed {
return nil, fmt.Errorf("session %q not found", params.ID)
Expand All @@ -274,6 +287,9 @@ func (p *ArthasPlugin) exec(ctx context.Context, req sdk.InvokeCtx) (any, error)
if params.SessionID == "" || params.Command == "" {
return nil, fmt.Errorf("sessionId and command are required")
}
if _, err := p.sessionForConfig(ctx, req, params.SessionID); err != nil {
return nil, err
}
return p.execCommand(ctx, params.SessionID, params.Command)
}

Expand Down Expand Up @@ -324,6 +340,50 @@ func (p *ArthasPlugin) execCommand(ctx context.Context, sessionID, command strin
return out, nil
}

func (p *ArthasPlugin) sessionForConfig(ctx context.Context, req sdk.InvokeCtx, id string) (*arthas.Session, error) {
sess, ok := p.sessions.Get(id)
if !ok {
return nil, fmt.Errorf("session %q not found", id)
}
pods, err := p.currentPods(ctx, req)
if err != nil {
return nil, err
}
if !sessionInPods(sess.Namespace, sess.Pod, sess.Container, pods) {
return nil, fmt.Errorf("session %q does not belong to the current config item", id)
}
return sess, nil
}

func (p *ArthasPlugin) currentPods(ctx context.Context, req sdk.InvokeCtx) ([]RunningPod, error) {
target, err := targetFromConfig(ctx, req.Host, req.ConfigItemID)
if err != nil {
return nil, err
}
cli, err := p.clients.Client(ctx, req.Host)
if err != nil {
return nil, err
}
return listRunningPodsForTarget(ctx, cli, target)
}

func sessionInPods(namespace, pod, container string, pods []RunningPod) bool {
for _, p := range pods {
if p.Namespace != namespace || p.Name != pod {
continue
}
if container == "" {
return true
}
for _, c := range p.Containers {
if c == container {
return true
}
}
}
return false
}

func targetFromConfig(ctx context.Context, host sdk.HostClient, configID string) (TargetRef, error) {
if host == nil || configID == "" {
return TargetRef{}, fmt.Errorf("config_id is required")
Expand Down
29 changes: 15 additions & 14 deletions arthas/ui-src/src/pages/ArthasPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ interface SessionCreateJob {
finishedAt?: string;
}

const SESSIONS_KEY = ["arthas", "sessions"] as const;
const sessionsKey = (configID: string) => ["arthas", configID, "sessions"] as const;

export function ArthasPage() {
const configID = configIDFromURL();
Expand All @@ -52,8 +52,9 @@ export function ArthasPage() {
const qc = useQueryClient();

const sessionsQ = useQuery({
queryKey: SESSIONS_KEY,
queryKey: sessionsKey(configID),
queryFn: () => callOp<ArthasSession[]>("sessions-list"),
enabled: !!configID,
refetchInterval: 5_000,
});

Expand All @@ -68,7 +69,7 @@ export function ArthasPage() {
mutationFn: (body: Record<string, unknown>) => callOp<SessionCreateJob>("session-create", body),
onSuccess: (job) => {
if (job.status === "running" && job.session) {
qc.invalidateQueries({ queryKey: SESSIONS_KEY });
qc.invalidateQueries({ queryKey: sessionsKey(configID) });
setSelectedId(job.session.id);
toastManager.add({ title: `Arthas session started on ${job.session.pod}`, type: "success" });
return;
Expand All @@ -89,20 +90,20 @@ export function ArthasPage() {
const job = createStatusQ.data;
if (!job || !createJobId) return;
if (job.status === "running" && job.session) {
qc.invalidateQueries({ queryKey: SESSIONS_KEY });
qc.invalidateQueries({ queryKey: sessionsKey(configID) });
setSelectedId(job.session.id);
setCreateJobId(null);
toastManager.add({ title: `Arthas session started on ${job.session.pod}`, type: "success" });
} else if (job.status === "failed") {
setCreateJobId(null);
toastManager.add({ title: job.error ? `Failed to start Arthas session: ${job.error}` : "Failed to start Arthas session", type: "error" });
}
}, [createJobId, createStatusQ.data, qc]);
}, [configID, createJobId, createStatusQ.data, qc]);

const del = useMutation({
mutationFn: (id: string) => callOp("session-delete", { id }),
onSuccess: (_, id) => {
qc.invalidateQueries({ queryKey: SESSIONS_KEY });
qc.invalidateQueries({ queryKey: sessionsKey(configID) });
if (selectedId === id) setSelectedId(null);
},
});
Expand Down Expand Up @@ -232,7 +233,7 @@ function SessionMenu({
<div className="max-h-96 overflow-auto">
{targets.map((target) => (
<div
key={`${target.pod.name}/${target.container}`}
key={`${target.pod.namespace}/${target.pod.name}/${target.container}`}
className={`grid grid-cols-[minmax(0,1fr)_auto] items-center gap-3 rounded px-2 py-2 hover:bg-muted ${
selectedId === target.session?.id ? "bg-muted" : ""
}`}
Expand Down Expand Up @@ -303,25 +304,25 @@ type PodSessionTarget = {
};

function sessionTargets(pods: RunningPod[], sessions: ArthasSession[]): PodSessionTarget[] {
const sessionsByPod = new Map<string, ArthasSession>();
const sessionsByTarget = new Map<string, ArthasSession>();
for (const session of sessions) {
if (!sessionsByPod.has(session.pod)) sessionsByPod.set(session.pod, session);
sessionsByTarget.set(targetKey(session.namespace, session.pod, session.container), session);
}

return pods.flatMap((pod) => {
const session = sessionsByPod.get(pod.name);
if (session) {
return [{ pod, container: session.container, session }];
}

const containers = pod.containers.length > 0 ? pod.containers : [""];
return containers.map((container) => ({
pod,
container,
session: sessionsByTarget.get(targetKey(pod.namespace, pod.name, container)),
}));
});
}

function targetKey(namespace: string, pod: string, container: string): string {
return `${namespace}/${pod}/${container}`;
}

function formatSessionTime(startedAt: string): string {
const date = new Date(startedAt);
if (Number.isNaN(date.getTime())) return "";
Expand Down
2 changes: 1 addition & 1 deletion arthas/ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
color: rgb(100 116 139);
}
</style>
<script type="module" crossorigin src="./assets/index-aVOcOYfw.js"></script>
<script type="module" crossorigin src="./assets/index-DgXPewQm.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-Txor7tpi.css">
</head>
<body class="bg-background text-foreground">
Expand Down
2 changes: 1 addition & 1 deletion arthas/ui_checksum.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 13 additions & 3 deletions inspektor-gadget/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,12 @@ func (p *InspektorGadgetPlugin) httpSession(w http.ResponseWriter, r *http.Reque
http.Error(w, "missing session id", http.StatusBadRequest)
return
}
sess, ok := p.sessions.Get(id)
if !ok {
http.Error(w, "session not found", http.StatusNotFound)
sess, err := p.sessionForConfig(r.Context(), sdk.InvokeCtx{
ConfigItemID: configItemIDFromRequest(r),
Host: sdk.HostClientFromContext(r.Context()),
}, id)
if err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
switch tail {
Expand All @@ -80,6 +83,13 @@ func (p *InspektorGadgetPlugin) httpSession(w http.ResponseWriter, r *http.Reque
}
}

func configItemIDFromRequest(r *http.Request) string {
if id := sdk.ConfigItemIDFromContext(r.Context()); id != "" {
return id
}
return r.URL.Query().Get("config_id")
}

func streamSessionEvents(w http.ResponseWriter, r *http.Request, sess *TraceSession) {
flusher, ok := w.(http.Flusher)
if !ok {
Expand Down
82 changes: 72 additions & 10 deletions inspektor-gadget/ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,36 +36,47 @@ func (p *InspektorGadgetPlugin) tracesList(_ context.Context, _ sdk.InvokeCtx) (
return supportedGadgets(p.settings.GadgetTag), nil
}

func (p *InspektorGadgetPlugin) traceList(_ context.Context, _ sdk.InvokeCtx) (any, error) {
return p.sessions.List(), nil
func (p *InspektorGadgetPlugin) traceList(ctx context.Context, req sdk.InvokeCtx) (any, error) {
pods, err := p.currentPods(ctx, req)
if err != nil {
return nil, err
}
sessions := p.sessions.List()
out := make([]*TraceSession, 0, len(sessions))
for i := range sessions {
if traceTargetInPods(sessions[i].Target, pods) {
out = append(out, &sessions[i])
}
}
return out, nil
}

func (p *InspektorGadgetPlugin) traceEvents(_ context.Context, req sdk.InvokeCtx) (any, error) {
func (p *InspektorGadgetPlugin) traceEvents(ctx context.Context, req sdk.InvokeCtx) (any, error) {
var params TraceEventsParams
if err := json.Unmarshal(req.ParamsJSON, &params); err != nil {
return nil, fmt.Errorf("decode params: %w", err)
}
if params.ID == "" {
return nil, fmt.Errorf("id is required")
}
sess, ok := p.sessions.Get(params.ID)
if !ok {
return nil, fmt.Errorf("session %q not found", params.ID)
sess, err := p.sessionForConfig(ctx, req, params.ID)
if err != nil {
return nil, err
}
return sess.Events(), nil
}

func (p *InspektorGadgetPlugin) traceStop(_ context.Context, req sdk.InvokeCtx) (any, error) {
func (p *InspektorGadgetPlugin) traceStop(ctx context.Context, req sdk.InvokeCtx) (any, error) {
var params TraceStopParams
if err := json.Unmarshal(req.ParamsJSON, &params); err != nil {
return nil, fmt.Errorf("decode params: %w", err)
}
if params.ID == "" {
return nil, fmt.Errorf("id is required")
}
sess, ok := p.sessions.Get(params.ID)
if !ok {
return nil, fmt.Errorf("session %q not found", params.ID)
sess, err := p.sessionForConfig(ctx, req, params.ID)
if err != nil {
return nil, err
}
sess.Stop()
return sess.Snapshot(), nil
Expand Down Expand Up @@ -161,6 +172,57 @@ func (p *InspektorGadgetPlugin) traceStart(ctx context.Context, req sdk.InvokeCt
return session.Snapshot(), nil
}

func (p *InspektorGadgetPlugin) sessionForConfig(ctx context.Context, req sdk.InvokeCtx, id string) (*TraceSession, error) {
sess, ok := p.sessions.Get(id)
if !ok {
return nil, fmt.Errorf("session %q not found", id)
}
if req.ConfigItemID == "" {
return sess, nil
}
pods, err := p.currentPods(ctx, req)
if err != nil {
return nil, err
}
if !traceTargetInPods(sess.Target, pods) {
return nil, fmt.Errorf("session %q does not belong to the current config item", id)
}
return sess, nil
}

func (p *InspektorGadgetPlugin) currentPods(ctx context.Context, req sdk.InvokeCtx) ([]RunningPod, error) {
target, err := targetFromConfig(ctx, req.Host, req.ConfigItemID)
if err != nil {
return nil, err
}
cli, err := p.clients.Client(ctx, req.Host)
if err != nil {
return nil, err
}
pods, err := listRunningPodsForTarget(ctx, cli, target)
if err != nil {
return nil, err
}
return pods, nil
}

func traceTargetInPods(target TraceTarget, pods []RunningPod) bool {
for _, pod := range pods {
if pod.Namespace != target.Namespace || pod.Name != target.Pod {
continue
}
if target.Container == "" {
return true
}
for _, container := range pod.Containers {
if container == target.Container {
return true
}
}
}
return false
}

func normalizeTraceOptions(params TraceStartParams) (map[string]any, error) {
options := map[string]any{}
for k, v := range params.Options {
Expand Down
12 changes: 11 additions & 1 deletion inspektor-gadget/ui-src/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,8 @@ function App() {
setSessions(nextSessions);
if (!selectedSession && nextSessions.length > 0) {
setSelectedSession(nextSessions[0].id);
} else if (selectedSession && !nextSessions.some((session) => session.id === selectedSession)) {
setSelectedSession(nextSessions[0]?.id ?? "");
}
}

Expand All @@ -225,7 +227,15 @@ function App() {

useEffect(() => {
const timer = window.setInterval(() => {
invoke<Session[]>("trace-list").then(setSessions).catch(() => undefined);
invoke<Session[]>("trace-list")
.then((nextSessions) => {
setSessions(nextSessions);
setSelectedSession((current) => {
if (!current) return nextSessions[0]?.id ?? "";
return nextSessions.some((session) => session.id === current) ? current : nextSessions[0]?.id ?? "";
});
})
.catch(() => undefined);
}, 5000);
return () => window.clearInterval(timer);
}, []);
Expand Down
2 changes: 1 addition & 1 deletion inspektor-gadget/ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Inspektor Gadget</title>
<script type="module" crossorigin src="./assets/index-ByQx19q4.js"></script>
<script type="module" crossorigin src="./assets/index-CHABs5cB.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-icX4j3sR.css">
</head>
<body>
Expand Down
2 changes: 1 addition & 1 deletion inspektor-gadget/ui_checksum.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading