-
Notifications
You must be signed in to change notification settings - Fork 875
feat: add endpoint to get listening ports in agent #4260
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
ed21d4a
d4663fa
3f741c6
4fbc0ff
0f7b8dc
ea2027e
e2973ba
d9635a8
3962635
4ea68bc
d4b7ce9
b084277
0960944
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
package agent | ||
|
||
import ( | ||
"net/http" | ||
"runtime" | ||
"sync" | ||
"time" | ||
|
||
"github.com/cakturk/go-netstat/netstat" | ||
"github.com/go-chi/chi" | ||
"golang.org/x/xerrors" | ||
|
||
"github.com/coder/coder/coderd/httpapi" | ||
"github.com/coder/coder/codersdk" | ||
) | ||
|
||
func (*agent) statisticsHandler() http.Handler { | ||
r := chi.NewRouter() | ||
r.Get("/", func(rw http.ResponseWriter, r *http.Request) { | ||
httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.Response{ | ||
Message: "Hello from the agent!", | ||
}) | ||
}) | ||
|
||
lp := &listeningPortsHandler{} | ||
r.Get("/api/v0/listening-ports", lp.handler) | ||
|
||
return r | ||
} | ||
|
||
type listeningPortsHandler struct { | ||
mut sync.Mutex | ||
ports []codersdk.ListeningPort | ||
mtime time.Time | ||
} | ||
|
||
func (lp *listeningPortsHandler) getListeningPorts() ([]codersdk.ListeningPort, error) { | ||
lp.mut.Lock() | ||
defer lp.mut.Unlock() | ||
|
||
if runtime.GOOS != "linux" && runtime.GOOS != "windows" { | ||
// Can't scan for ports on non-linux or non-windows systems at the | ||
// moment. The UI will not show any "no ports found" message to the | ||
// user, so the user won't suspect a thing. | ||
return []codersdk.ListeningPort{}, nil | ||
} | ||
|
||
if time.Since(lp.mtime) < time.Second { | ||
// copy | ||
ports := make([]codersdk.ListeningPort, len(lp.ports)) | ||
copy(ports, lp.ports) | ||
return ports, nil | ||
} | ||
|
||
tabs, err := netstat.TCPSocks(func(s *netstat.SockTabEntry) bool { | ||
return s.State == netstat.Listen | ||
}) | ||
if err != nil { | ||
return nil, xerrors.Errorf("scan listening ports: %w", err) | ||
} | ||
|
||
ports := []codersdk.ListeningPort{} | ||
for _, tab := range tabs { | ||
ports = append(ports, codersdk.ListeningPort{ | ||
ProcessName: tab.Process.Name, | ||
Network: codersdk.ListeningPortNetworkTCP, | ||
Port: tab.LocalAddr.Port, | ||
}) | ||
} | ||
|
||
lp.ports = ports | ||
lp.mtime = time.Now() | ||
|
||
// copy | ||
ports = make([]codersdk.ListeningPort, len(lp.ports)) | ||
copy(ports, lp.ports) | ||
return ports, nil | ||
} | ||
|
||
// handler returns a list of listening ports. This is tested by coderd's | ||
// TestWorkspaceAgentListeningPorts test. | ||
func (lp *listeningPortsHandler) handler(rw http.ResponseWriter, r *http.Request) { | ||
ports, err := lp.getListeningPorts() | ||
if err != nil { | ||
httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{ | ||
Message: "Could not scan for listening ports.", | ||
Detail: err.Error(), | ||
}) | ||
return | ||
} | ||
|
||
httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.ListeningPortsResponse{ | ||
Ports: ports, | ||
}) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -218,6 +218,37 @@ func (api *API) workspaceAgentPTY(rw http.ResponseWriter, r *http.Request) { | |
_, _ = io.Copy(ptNetConn, wsNetConn) | ||
} | ||
|
||
func (api *API) workspaceAgentListeningPorts(rw http.ResponseWriter, r *http.Request) { | ||
ctx := r.Context() | ||
workspace := httpmw.WorkspaceParam(r) | ||
workspaceAgent := httpmw.WorkspaceAgentParam(r) | ||
if !api.Authorize(r, rbac.ActionRead, workspace) { | ||
httpapi.ResourceNotFound(rw) | ||
return | ||
} | ||
|
||
agentConn, release, err := api.workspaceAgentCache.Acquire(r, workspaceAgent.ID) | ||
if err != nil { | ||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ | ||
Message: "Internal error dialing workspace agent.", | ||
Detail: err.Error(), | ||
}) | ||
return | ||
} | ||
defer release() | ||
deansheather marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
portsResponse, err := agentConn.ListeningPorts(ctx) | ||
if err != nil { | ||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ | ||
Message: "Internal error fetching listening ports.", | ||
Detail: err.Error(), | ||
}) | ||
return | ||
} | ||
Comment on lines
+246
to
+263
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do you think about pushing port data instead of pulling it? This seems like it could lead to a lot of workspace traffic with page reloads, and we've learned from v1 that dynamic data like this (especially on main pages) can be sketchy. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will only be loaded when people click the port forward button. There will be more traffic if workspaces push it to coderd rather than if we load it when the user clicks the button which won't be that often. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To add to this, I talked with dean and I think the syscall overhead is way too much for a responsive push system for such a small feature like this. Because we cache the response for 1 second on the agent side this already has built in protection for the workspace. |
||
|
||
httpapi.Write(ctx, rw, http.StatusOK, portsResponse) | ||
} | ||
|
||
func (api *API) dialWorkspaceAgentTailnet(r *http.Request, agentID uuid.UUID) (*codersdk.AgentConn, error) { | ||
clientConn, serverConn := net.Pipe() | ||
go func() { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of creating an API for statistics, we should just handle this single port for right now on a handler. We probably don't even need Chi and can just directly serve it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@f0ssel and I would like to convert the stats websocket code in the agent to be in this webserver too, which is why it has path based routing. I've only reserved 8 ports for Coder so we can't just create a new http server for each function