Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit1386465

Browse files
authored
feat: add endpoint to get listening ports in agent (#4260)
1 parentbbe2baf commit1386465

15 files changed

+501
-1
lines changed

‎agent/agent.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"fmt"
1111
"io"
1212
"net"
13+
"net/http"
1314
"net/netip"
1415
"os"
1516
"os/exec"
@@ -206,6 +207,7 @@ func (a *agent) runTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) {
206207
goa.sshServer.HandleConn(a.stats.wrapConn(conn))
207208
}
208209
}()
210+
209211
reconnectingPTYListener,err:=a.network.Listen("tcp",":"+strconv.Itoa(codersdk.TailnetReconnectingPTYPort))
210212
iferr!=nil {
211213
a.logger.Critical(ctx,"listen for reconnecting pty",slog.Error(err))
@@ -240,6 +242,7 @@ func (a *agent) runTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) {
240242
goa.handleReconnectingPTY(ctx,msg,conn)
241243
}
242244
}()
245+
243246
speedtestListener,err:=a.network.Listen("tcp",":"+strconv.Itoa(codersdk.TailnetSpeedtestPort))
244247
iferr!=nil {
245248
a.logger.Critical(ctx,"listen for speedtest",slog.Error(err))
@@ -261,6 +264,31 @@ func (a *agent) runTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) {
261264
}()
262265
}
263266
}()
267+
268+
statisticsListener,err:=a.network.Listen("tcp",":"+strconv.Itoa(codersdk.TailnetStatisticsPort))
269+
iferr!=nil {
270+
a.logger.Critical(ctx,"listen for statistics",slog.Error(err))
271+
return
272+
}
273+
gofunc() {
274+
deferstatisticsListener.Close()
275+
server:=&http.Server{
276+
Handler:a.statisticsHandler(),
277+
ReadTimeout:20*time.Second,
278+
ReadHeaderTimeout:20*time.Second,
279+
WriteTimeout:20*time.Second,
280+
ErrorLog:slog.Stdlib(ctx,a.logger.Named("statistics_http_server"),slog.LevelInfo),
281+
}
282+
gofunc() {
283+
<-ctx.Done()
284+
_=server.Close()
285+
}()
286+
287+
err=server.Serve(statisticsListener)
288+
iferr!=nil&&!xerrors.Is(err,http.ErrServerClosed)&&!strings.Contains(err.Error(),"use of closed network connection") {
289+
a.logger.Critical(ctx,"serve statistics HTTP server",slog.Error(err))
290+
}
291+
}()
264292
}
265293

266294
// runCoordinator listens for nodes and updates the self-node as it changes.

‎agent/ports_supported.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//go:build linux || windows
2+
// +build linux windows
3+
4+
package agent
5+
6+
import (
7+
"time"
8+
9+
"github.com/cakturk/go-netstat/netstat"
10+
"golang.org/x/xerrors"
11+
12+
"github.com/coder/coder/codersdk"
13+
)
14+
15+
func (lp*listeningPortsHandler)getListeningPorts() ([]codersdk.ListeningPort,error) {
16+
lp.mut.Lock()
17+
deferlp.mut.Unlock()
18+
19+
iftime.Since(lp.mtime)<time.Second {
20+
// copy
21+
ports:=make([]codersdk.ListeningPort,len(lp.ports))
22+
copy(ports,lp.ports)
23+
returnports,nil
24+
}
25+
26+
tabs,err:=netstat.TCPSocks(func(s*netstat.SockTabEntry)bool {
27+
returns.State==netstat.Listen
28+
})
29+
iferr!=nil {
30+
returnnil,xerrors.Errorf("scan listening ports: %w",err)
31+
}
32+
33+
seen:=make(map[uint16]struct{},len(tabs))
34+
ports:= []codersdk.ListeningPort{}
35+
for_,tab:=rangetabs {
36+
iftab.LocalAddr==nil||tab.LocalAddr.Port<uint16(codersdk.MinimumListeningPort) {
37+
continue
38+
}
39+
40+
// Don't include ports that we've already seen. This can happen on
41+
// Windows, and maybe on Linux if you're using a shared listener socket.
42+
if_,ok:=seen[tab.LocalAddr.Port];ok {
43+
continue
44+
}
45+
seen[tab.LocalAddr.Port]=struct{}{}
46+
47+
procName:=""
48+
iftab.Process!=nil {
49+
procName=tab.Process.Name
50+
}
51+
ports=append(ports, codersdk.ListeningPort{
52+
ProcessName:procName,
53+
Network:codersdk.ListeningPortNetworkTCP,
54+
Port:tab.LocalAddr.Port,
55+
})
56+
}
57+
58+
lp.ports=ports
59+
lp.mtime=time.Now()
60+
61+
// copy
62+
ports=make([]codersdk.ListeningPort,len(lp.ports))
63+
copy(ports,lp.ports)
64+
returnports,nil
65+
}

‎agent/ports_unsupported.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//go:build !linux && !windows
2+
// +build !linux,!windows
3+
4+
package agent
5+
6+
import"github.com/coder/coder/codersdk"
7+
8+
func (lp*listeningPortsHandler)getListeningPorts() ([]codersdk.ListeningPort,error) {
9+
// Can't scan for ports on non-linux or non-windows systems at the moment.
10+
// The UI will not show any "no ports found" message to the user, so the
11+
// user won't suspect a thing.
12+
return []codersdk.ListeningPort{},nil
13+
}

‎agent/statsendpoint.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package agent
2+
3+
import (
4+
"net/http"
5+
"sync"
6+
"time"
7+
8+
"github.com/go-chi/chi"
9+
10+
"github.com/coder/coder/coderd/httpapi"
11+
"github.com/coder/coder/codersdk"
12+
)
13+
14+
func (*agent)statisticsHandler() http.Handler {
15+
r:=chi.NewRouter()
16+
r.Get("/",func(rw http.ResponseWriter,r*http.Request) {
17+
httpapi.Write(r.Context(),rw,http.StatusOK, codersdk.Response{
18+
Message:"Hello from the agent!",
19+
})
20+
})
21+
22+
lp:=&listeningPortsHandler{}
23+
r.Get("/api/v0/listening-ports",lp.handler)
24+
25+
returnr
26+
}
27+
28+
typelisteningPortsHandlerstruct {
29+
mut sync.Mutex
30+
ports []codersdk.ListeningPort
31+
mtime time.Time
32+
}
33+
34+
// handler returns a list of listening ports. This is tested by coderd's
35+
// TestWorkspaceAgentListeningPorts test.
36+
func (lp*listeningPortsHandler)handler(rw http.ResponseWriter,r*http.Request) {
37+
ports,err:=lp.getListeningPorts()
38+
iferr!=nil {
39+
httpapi.Write(r.Context(),rw,http.StatusInternalServerError, codersdk.Response{
40+
Message:"Could not scan for listening ports.",
41+
Detail:err.Error(),
42+
})
43+
return
44+
}
45+
46+
httpapi.Write(r.Context(),rw,http.StatusOK, codersdk.ListeningPortsResponse{
47+
Ports:ports,
48+
})
49+
}

‎coderd/coderd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,7 @@ func New(options *Options) *API {
438438
)
439439
r.Get("/",api.workspaceAgent)
440440
r.Get("/pty",api.workspaceAgentPTY)
441+
r.Get("/listening-ports",api.workspaceAgentListeningPorts)
441442
r.Get("/connection",api.workspaceAgentConnection)
442443
r.Get("/coordinate",api.workspaceAgentClientCoordinate)
443444
// TODO: This can be removed in October. It allows for a friendly

‎coderd/workspaceagents.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,52 @@ func (api *API) workspaceAgentPTY(rw http.ResponseWriter, r *http.Request) {
219219
_,_=io.Copy(ptNetConn,wsNetConn)
220220
}
221221

222+
func (api*API)workspaceAgentListeningPorts(rw http.ResponseWriter,r*http.Request) {
223+
ctx:=r.Context()
224+
workspace:=httpmw.WorkspaceParam(r)
225+
workspaceAgent:=httpmw.WorkspaceAgentParam(r)
226+
if!api.Authorize(r,rbac.ActionRead,workspace) {
227+
httpapi.ResourceNotFound(rw)
228+
return
229+
}
230+
231+
apiAgent,err:=convertWorkspaceAgent(api.DERPMap,api.TailnetCoordinator,workspaceAgent,nil,api.AgentInactiveDisconnectTimeout)
232+
iferr!=nil {
233+
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
234+
Message:"Internal error reading workspace agent.",
235+
Detail:err.Error(),
236+
})
237+
return
238+
}
239+
ifapiAgent.Status!=codersdk.WorkspaceAgentConnected {
240+
httpapi.Write(ctx,rw,http.StatusPreconditionRequired, codersdk.Response{
241+
Message:fmt.Sprintf("Agent state is %q, it must be in the %q state.",apiAgent.Status,codersdk.WorkspaceAgentConnected),
242+
})
243+
return
244+
}
245+
246+
agentConn,release,err:=api.workspaceAgentCache.Acquire(r,workspaceAgent.ID)
247+
iferr!=nil {
248+
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
249+
Message:"Internal error dialing workspace agent.",
250+
Detail:err.Error(),
251+
})
252+
return
253+
}
254+
deferrelease()
255+
256+
portsResponse,err:=agentConn.ListeningPorts(ctx)
257+
iferr!=nil {
258+
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
259+
Message:"Internal error fetching listening ports.",
260+
Detail:err.Error(),
261+
})
262+
return
263+
}
264+
265+
httpapi.Write(ctx,rw,http.StatusOK,portsResponse)
266+
}
267+
222268
func (api*API)dialWorkspaceAgentTailnet(r*http.Request,agentID uuid.UUID) (*codersdk.AgentConn,error) {
223269
clientConn,serverConn:=net.Pipe()
224270
gofunc() {

‎coderd/workspaceagents_test.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import (
44
"bufio"
55
"context"
66
"encoding/json"
7+
"net"
78
"runtime"
9+
"strconv"
810
"strings"
911
"testing"
1012
"time"
@@ -363,6 +365,133 @@ func TestWorkspaceAgentPTY(t *testing.T) {
363365
expectLine(matchEchoOutput)
364366
}
365367

368+
funcTestWorkspaceAgentListeningPorts(t*testing.T) {
369+
t.Parallel()
370+
client:=coderdtest.New(t,&coderdtest.Options{
371+
IncludeProvisionerDaemon:true,
372+
})
373+
coderdPort,err:=strconv.Atoi(client.URL.Port())
374+
require.NoError(t,err)
375+
376+
user:=coderdtest.CreateFirstUser(t,client)
377+
authToken:=uuid.NewString()
378+
version:=coderdtest.CreateTemplateVersion(t,client,user.OrganizationID,&echo.Responses{
379+
Parse:echo.ParseComplete,
380+
ProvisionDryRun:echo.ProvisionComplete,
381+
Provision: []*proto.Provision_Response{{
382+
Type:&proto.Provision_Response_Complete{
383+
Complete:&proto.Provision_Complete{
384+
Resources: []*proto.Resource{{
385+
Name:"example",
386+
Type:"aws_instance",
387+
Agents: []*proto.Agent{{
388+
Id:uuid.NewString(),
389+
Auth:&proto.Agent_Token{
390+
Token:authToken,
391+
},
392+
}},
393+
}},
394+
},
395+
},
396+
}},
397+
})
398+
template:=coderdtest.CreateTemplate(t,client,user.OrganizationID,version.ID)
399+
coderdtest.AwaitTemplateVersionJob(t,client,version.ID)
400+
workspace:=coderdtest.CreateWorkspace(t,client,user.OrganizationID,template.ID)
401+
coderdtest.AwaitWorkspaceBuildJob(t,client,workspace.LatestBuild.ID)
402+
403+
agentClient:=codersdk.New(client.URL)
404+
agentClient.SessionToken=authToken
405+
agentCloser:=agent.New(agent.Options{
406+
FetchMetadata:agentClient.WorkspaceAgentMetadata,
407+
CoordinatorDialer:agentClient.ListenWorkspaceAgentTailnet,
408+
Logger:slogtest.Make(t,nil).Named("agent").Leveled(slog.LevelDebug),
409+
})
410+
t.Cleanup(func() {
411+
_=agentCloser.Close()
412+
})
413+
resources:=coderdtest.AwaitWorkspaceAgents(t,client,workspace.ID)
414+
415+
t.Run("LinuxAndWindows",func(t*testing.T) {
416+
t.Parallel()
417+
ifruntime.GOOS!="linux"&&runtime.GOOS!="windows" {
418+
t.Skip("only runs on linux and windows")
419+
return
420+
}
421+
422+
ctx,cancel:=context.WithTimeout(context.Background(),testutil.WaitLong)
423+
defercancel()
424+
425+
// Create a TCP listener on a random port that we expect to see in the
426+
// response.
427+
l,err:=net.Listen("tcp","localhost:0")
428+
require.NoError(t,err)
429+
deferl.Close()
430+
tcpAddr,_:=l.Addr().(*net.TCPAddr)
431+
432+
// List ports and ensure that the port we expect to see is there.
433+
res,err:=client.WorkspaceAgentListeningPorts(ctx,resources[0].Agents[0].ID)
434+
require.NoError(t,err)
435+
436+
var (
437+
expected=map[uint16]bool{
438+
// expect the listener we made
439+
uint16(tcpAddr.Port):false,
440+
// expect the coderdtest server
441+
uint16(coderdPort):false,
442+
}
443+
)
444+
for_,port:=rangeres.Ports {
445+
ifport.Network==codersdk.ListeningPortNetworkTCP {
446+
ifval,ok:=expected[port.Port];ok {
447+
ifval {
448+
t.Fatalf("expected to find TCP port %d only once in response",port.Port)
449+
}
450+
}
451+
expected[port.Port]=true
452+
}
453+
}
454+
forport,found:=rangeexpected {
455+
if!found {
456+
t.Fatalf("expected to find TCP port %d in response",port)
457+
}
458+
}
459+
460+
// Close the listener and check that the port is no longer in the response.
461+
require.NoError(t,l.Close())
462+
time.Sleep(2*time.Second)// avoid cache
463+
res,err=client.WorkspaceAgentListeningPorts(ctx,resources[0].Agents[0].ID)
464+
require.NoError(t,err)
465+
466+
for_,port:=rangeres.Ports {
467+
ifport.Network==codersdk.ListeningPortNetworkTCP&&port.Port==uint16(tcpAddr.Port) {
468+
t.Fatalf("expected to not find TCP port %d in response",tcpAddr.Port)
469+
}
470+
}
471+
})
472+
473+
t.Run("Darwin",func(t*testing.T) {
474+
t.Parallel()
475+
ifruntime.GOOS!="darwin" {
476+
t.Skip("only runs on darwin")
477+
return
478+
}
479+
480+
ctx,cancel:=context.WithTimeout(context.Background(),testutil.WaitLong)
481+
defercancel()
482+
483+
// Create a TCP listener on a random port.
484+
l,err:=net.Listen("tcp","localhost:0")
485+
require.NoError(t,err)
486+
deferl.Close()
487+
488+
// List ports and ensure that the list is empty because we're on darwin.
489+
res,err:=client.WorkspaceAgentListeningPorts(ctx,resources[0].Agents[0].ID)
490+
require.NoError(t,err)
491+
require.Len(t,res.Ports,0)
492+
})
493+
}
494+
366495
funcTestWorkspaceAgentAppHealth(t*testing.T) {
367496
t.Parallel()
368497
client:=coderdtest.New(t,&coderdtest.Options{

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp