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
15 changes: 15 additions & 0 deletions golang/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ func (p *GolangPlugin) httpProxyPprof(w http.ResponseWriter, r *http.Request) {
http.Error(w, "session not found", http.StatusNotFound)
return
}
if !sessionMatchesConfig(sess, configItemIDFromRequest(r)) {
http.Error(w, "session does not belong to the current config item", http.StatusForbidden)
return
}
if !sess.PprofAvailable || sess.PprofLocal == 0 {
http.Error(w, "pprof is not available for this session", http.StatusBadRequest)
return
Expand Down Expand Up @@ -83,6 +87,10 @@ func (p *GolangPlugin) httpProfile(w http.ResponseWriter, r *http.Request) {
http.Error(w, "session not found", http.StatusNotFound)
return
}
if !sessionMatchesConfig(sess, configItemIDFromRequest(r)) {
http.Error(w, "session does not belong to the current config item", http.StatusForbidden)
return
}

runIDOrKind, subPath, _ := strings.Cut(tail, "/")
if run, ok := p.profiles.Get(runIDOrKind); ok {
Expand Down Expand Up @@ -139,6 +147,13 @@ func (p *GolangPlugin) proxyProfileViewer(w http.ResponseWriter, r *http.Request
proxy.ServeHTTP(w, r)
}

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

func operationSubpath(r *http.Request, operation string) string {
if p := strings.Trim(r.URL.Query().Get("path"), "/"); p != "" {
return p
Expand Down
6 changes: 3 additions & 3 deletions golang/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ var _ = ginkgo.Describe("HTTP handler", func() {

ginkgo.It("validates profile paths", func() {
p := newPlugin()
sess := NewSession("default", "pod", "app", "app-0", "app", nil)
sess := NewSession("", "default", "pod", "app", "app-0", "app", nil)
p.sessions.Add(sess)
req := httptest.NewRequest(http.MethodGet, "/__mc/operations/profiles?path="+sess.ID+"/unknown", nil)
rec := httptest.NewRecorder()
Expand All @@ -34,7 +34,7 @@ var _ = ginkgo.Describe("HTTP handler", func() {

ginkgo.It("serves completed profile runs from the registry", func() {
p := newPlugin()
sess := NewSession("default", "pod", "app", "app-0", "app", nil)
sess := NewSession("", "default", "pod", "app", "app-0", "app", nil)
p.sessions.Add(sess)
run, _ := NewProfileRun(sess.ID, "heap", "pprof", 30)
run.MarkDone([]byte("profile-bytes"), "pprof", nil)
Expand All @@ -53,7 +53,7 @@ var _ = ginkgo.Describe("HTTP handler", func() {

ginkgo.It("does not download running profile runs", func() {
p := newPlugin()
sess := NewSession("default", "pod", "app", "app-0", "app", nil)
sess := NewSession("", "default", "pod", "app", "app-0", "app", nil)
p.sessions.Add(sess)
run, _ := NewProfileRun(sess.ID, "cpu", "auto", 30)
p.profiles.Add(run)
Expand Down
54 changes: 37 additions & 17 deletions golang/ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,8 @@ func (p *GolangPlugin) podsList(ctx context.Context, req sdk.InvokeCtx) (any, er
return listRunningPodsForTarget(ctx, cli, target)
}

func (p *GolangPlugin) sessionsList(_ context.Context, _ sdk.InvokeCtx) (any, error) {
return p.sessions.List(), nil
func (p *GolangPlugin) sessionsList(_ context.Context, req sdk.InvokeCtx) (any, error) {
return p.sessions.List(req.ConfigItemID), nil
}

func (p *GolangPlugin) sessionCreate(ctx context.Context, req sdk.InvokeCtx) (any, error) {
Expand Down Expand Up @@ -211,7 +211,7 @@ func (p *GolangPlugin) sessionCreate(ctx context.Context, req sdk.InvokeCtx) (an
_ = fwd.Close()
return nil, fmt.Errorf("port-forward not ready: %w", err)
}
sess := NewSession(pod.Namespace, target.Kind, target.Name, pod.Name, container, func() error { return fwd.Close() })
sess := NewSession(req.ConfigItemID, pod.Namespace, target.Kind, target.Name, pod.Name, container, func() error { return fwd.Close() })
sess.PID = pid
if match, ok := firstWorkingGops(ctx, gopsCandidates); ok {
sess.GopsRemote = match.Remote
Expand Down Expand Up @@ -276,6 +276,9 @@ func (p *GolangPlugin) sessionDelete(_ context.Context, req sdk.InvokeCtx) (any,
if params.ID == "" {
return nil, fmt.Errorf("id is required")
}
if _, err := p.getSessionForConfig(params.ID, req.ConfigItemID); 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 @@ -288,7 +291,7 @@ func (p *GolangPlugin) sessionDelete(_ context.Context, req sdk.InvokeCtx) (any,
}

func (p *GolangPlugin) runtimeSnapshot(ctx context.Context, req sdk.InvokeCtx) (any, error) {
sess, err := p.sessionFromRequest(req.ParamsJSON)
sess, err := p.sessionFromRequest(req)
if err != nil {
return nil, err
}
Expand All @@ -307,7 +310,7 @@ func (p *GolangPlugin) runtimeSnapshot(ctx context.Context, req sdk.InvokeCtx) (
}

func (p *GolangPlugin) goroutines(ctx context.Context, req sdk.InvokeCtx) (any, error) {
sess, err := p.sessionFromRequest(req.ParamsJSON)
sess, err := p.sessionFromRequest(req)
if err != nil {
return nil, err
}
Expand All @@ -333,9 +336,9 @@ func (p *GolangPlugin) profileCollect(ctx context.Context, req sdk.InvokeCtx) (a
if err := json.Unmarshal(req.ParamsJSON, &params); err != nil {
return nil, fmt.Errorf("decode params: %w", err)
}
sess, ok := p.sessions.Get(params.SessionID)
if !ok {
return nil, fmt.Errorf("session %q not found", params.SessionID)
sess, err := p.getSessionForConfig(params.SessionID, req.ConfigItemID)
if err != nil {
return nil, err
}
kind := normalizeProfileKind(params.Kind)
if kind == "" {
Expand Down Expand Up @@ -369,9 +372,9 @@ func (p *GolangPlugin) profileStart(_ context.Context, req sdk.InvokeCtx) (any,
if err := json.Unmarshal(req.ParamsJSON, &params); err != nil {
return nil, fmt.Errorf("decode params: %w", err)
}
sess, ok := p.sessions.Get(params.SessionID)
if !ok {
return nil, fmt.Errorf("session %q not found", params.SessionID)
sess, err := p.getSessionForConfig(params.SessionID, req.ConfigItemID)
if err != nil {
return nil, err
}
kind := normalizeProfileKind(params.Kind)
if kind == "" {
Expand Down Expand Up @@ -412,6 +415,9 @@ func (p *GolangPlugin) profileStatus(_ context.Context, req sdk.InvokeCtx) (any,
if params.SessionID != "" && run.SessionID != params.SessionID {
return nil, fmt.Errorf("profile run %q does not belong to session %q", params.RunID, params.SessionID)
}
if _, err := p.getSessionForConfig(run.SessionID, req.ConfigItemID); err != nil {
return nil, err
}
return run.Snapshot(), nil
}

Expand All @@ -430,6 +436,9 @@ func (p *GolangPlugin) profileStop(_ context.Context, req sdk.InvokeCtx) (any, e
if params.SessionID != "" && run.SessionID != params.SessionID {
return nil, fmt.Errorf("profile run %q does not belong to session %q", params.RunID, params.SessionID)
}
if _, err := p.getSessionForConfig(run.SessionID, req.ConfigItemID); err != nil {
return nil, err
}
run.Stop()
return run.Snapshot(), nil
}
Expand All @@ -442,27 +451,38 @@ func (p *GolangPlugin) profileRunsList(_ context.Context, req sdk.InvokeCtx) (an
if params.SessionID == "" {
return nil, fmt.Errorf("sessionId is required")
}
if _, ok := p.sessions.Get(params.SessionID); !ok {
return nil, fmt.Errorf("session %q not found", params.SessionID)
if _, err := p.getSessionForConfig(params.SessionID, req.ConfigItemID); err != nil {
return nil, err
}
return p.profiles.List(params.SessionID), nil
}

func (p *GolangPlugin) sessionFromRequest(raw []byte) (*Session, error) {
func (p *GolangPlugin) sessionFromRequest(req sdk.InvokeCtx) (*Session, error) {
var params SessionIDParams
if err := json.Unmarshal(raw, &params); err != nil {
if err := json.Unmarshal(req.ParamsJSON, &params); err != nil {
return nil, fmt.Errorf("decode params: %w", err)
}
if params.SessionID == "" {
return nil, fmt.Errorf("sessionId is required")
}
sess, ok := p.sessions.Get(params.SessionID)
return p.getSessionForConfig(params.SessionID, req.ConfigItemID)
}

func (p *GolangPlugin) getSessionForConfig(sessionID, configItemID string) (*Session, error) {
sess, ok := p.sessions.Get(sessionID)
if !ok {
return nil, fmt.Errorf("session %q not found", params.SessionID)
return nil, fmt.Errorf("session %q not found", sessionID)
}
if !sessionMatchesConfig(sess, configItemID) {
return nil, fmt.Errorf("session %q does not belong to the current config item", sessionID)
}
return sess, nil
}

func sessionMatchesConfig(sess *Session, configItemID string) bool {
return configItemID == "" || sess.ConfigItemID == configItemID
}

func selectPodContainer(pods []RunningPod, podName, container string) (RunningPod, string, error) {
if len(pods) == 0 {
return RunningPod{}, "", fmt.Errorf("no ready pods found")
Expand Down
16 changes: 16 additions & 0 deletions golang/ops_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
package main

import (
"github.com/flanksource/incident-commander/plugin/sdk"
ginkgo "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var _ = ginkgo.Describe("sessions", func() {
ginkgo.It("lists only sessions for the current config item", func() {
p := newPlugin()
current := NewSession("config-a", "default", "pod", "app-a", "app-a-0", "app", nil)
other := NewSession("config-b", "default", "pod", "app-b", "app-b-0", "app", nil)
p.sessions.Add(current)
p.sessions.Add(other)

result, err := p.sessionsList(ginkgo.GinkgoT().Context(), sdk.InvokeCtx{ConfigItemID: "config-a"})

Expect(err).NotTo(HaveOccurred())
Expect(result).To(Equal([]Session{current.Snapshot()}))
})
})

var _ = ginkgo.Describe("port candidates", func() {
ginkgo.It("prefers discovered gops ports over defaults", func() {
Expect(gopsCandidatePorts(4321, 6061, []int{6061, 7070})).To(Equal([]int{4321}))
Expand Down
26 changes: 16 additions & 10 deletions golang/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

type Session struct {
ID string `json:"id"`
ConfigItemID string `json:"configItemId,omitempty"`
Namespace string `json:"namespace"`
Kind string `json:"kind"`
Name string `json:"name"`
Expand Down Expand Up @@ -44,6 +45,7 @@ func (s *Session) Stop() error {
func (s *Session) Snapshot() Session {
return Session{
ID: s.ID,
ConfigItemID: s.ConfigItemID,
Namespace: s.Namespace,
Kind: s.Kind,
Name: s.Name,
Expand Down Expand Up @@ -84,10 +86,13 @@ func (r *SessionRegistry) Get(id string) (*Session, bool) {
return s, ok
}

func (r *SessionRegistry) List() []Session {
func (r *SessionRegistry) List(configItemID string) []Session {
r.mu.RLock()
out := make([]Session, 0, len(r.sessions))
for _, s := range r.sessions {
if configItemID != "" && s.ConfigItemID != configItemID {
continue
}
out = append(out, s.Snapshot())
}
r.mu.RUnlock()
Expand All @@ -114,16 +119,17 @@ func (r *SessionRegistry) RunningCount() int {
return len(r.sessions)
}

func NewSession(namespace, kind, name, pod, container string, stop func() error) *Session {
func NewSession(configItemID, namespace, kind, name, pod, container string, stop func() error) *Session {
return &Session{
ID: newID(),
Namespace: namespace,
Kind: kind,
Name: name,
Pod: pod,
Container: container,
StartedAt: time.Now().UTC(),
stop: stop,
ID: newID(),
ConfigItemID: configItemID,
Namespace: namespace,
Kind: kind,
Name: name,
Pod: pod,
Container: container,
StartedAt: time.Now().UTC(),
stop: stop,
}
}

Expand Down
Loading
Loading