feat(site): use websocket connection for devcontainer updates (#18808)

Instead of polling every 10 seconds, we instead use a WebSocket
connection for more timely updates.
This commit is contained in:
Danielle Maywood
2025-07-14 21:35:35 +01:00
committed by GitHub
parent 7cf3263fbd
commit 43b0bb7f61
15 changed files with 1079 additions and 23 deletions
+47
View File
@@ -421,6 +421,19 @@ type WorkspaceAgentDevcontainer struct {
Error string `json:"error,omitempty"`
}
func (d WorkspaceAgentDevcontainer) Equals(other WorkspaceAgentDevcontainer) bool {
return d.ID == other.ID &&
d.Name == other.Name &&
d.WorkspaceFolder == other.WorkspaceFolder &&
d.Status == other.Status &&
d.Dirty == other.Dirty &&
(d.Container == nil && other.Container == nil ||
(d.Container != nil && other.Container != nil && d.Container.ID == other.Container.ID)) &&
(d.Agent == nil && other.Agent == nil ||
(d.Agent != nil && other.Agent != nil && *d.Agent == *other.Agent)) &&
d.Error == other.Error
}
// WorkspaceAgentDevcontainerAgent represents the sub agent for a
// devcontainer.
type WorkspaceAgentDevcontainerAgent struct {
@@ -520,6 +533,40 @@ func (c *Client) WorkspaceAgentListContainers(ctx context.Context, agentID uuid.
return cr, json.NewDecoder(res.Body).Decode(&cr)
}
func (c *Client) WatchWorkspaceAgentContainers(ctx context.Context, agentID uuid.UUID) (<-chan WorkspaceAgentListContainersResponse, io.Closer, error) {
reqURL, err := c.URL.Parse(fmt.Sprintf("/api/v2/workspaceagents/%s/containers/watch", agentID))
if err != nil {
return nil, nil, err
}
jar, err := cookiejar.New(nil)
if err != nil {
return nil, nil, xerrors.Errorf("create cookie jar: %w", err)
}
jar.SetCookies(reqURL, []*http.Cookie{{
Name: SessionTokenCookie,
Value: c.SessionToken(),
}})
conn, res, err := websocket.Dial(ctx, reqURL.String(), &websocket.DialOptions{
CompressionMode: websocket.CompressionDisabled,
HTTPClient: &http.Client{
Jar: jar,
Transport: c.HTTPClient.Transport,
},
})
if err != nil {
if res == nil {
return nil, nil, err
}
return nil, nil, ReadBodyAsError(res)
}
d := wsjson.NewDecoder[WorkspaceAgentListContainersResponse](conn, websocket.MessageText, c.logger)
return d.Chan(), d, nil
}
// WorkspaceAgentRecreateDevcontainer recreates the devcontainer with the given ID.
func (c *Client) WorkspaceAgentRecreateDevcontainer(ctx context.Context, agentID uuid.UUID, devcontainerID string) (Response, error) {
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/workspaceagents/%s/containers/devcontainers/%s/recreate", agentID, devcontainerID), nil)
+28
View File
@@ -20,10 +20,14 @@ import (
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/speedtest"
"cdr.dev/slog"
"github.com/coder/coder/v2/coderd/tracing"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/healthsdk"
"github.com/coder/coder/v2/codersdk/wsjson"
"github.com/coder/coder/v2/tailnet"
"github.com/coder/websocket"
)
// NewAgentConn creates a new WorkspaceAgentConn. `conn` may be unique
@@ -387,6 +391,30 @@ func (c *AgentConn) ListContainers(ctx context.Context) (codersdk.WorkspaceAgent
return resp, json.NewDecoder(res.Body).Decode(&resp)
}
func (c *AgentConn) WatchContainers(ctx context.Context, logger slog.Logger) (<-chan codersdk.WorkspaceAgentListContainersResponse, io.Closer, error) {
ctx, span := tracing.StartSpan(ctx)
defer span.End()
host := net.JoinHostPort(c.agentAddress().String(), strconv.Itoa(AgentHTTPAPIServerPort))
url := fmt.Sprintf("http://%s%s", host, "/api/v0/containers/watch")
conn, res, err := websocket.Dial(ctx, url, &websocket.DialOptions{
HTTPClient: c.apiClient(),
})
if err != nil {
if res == nil {
return nil, nil, err
}
return nil, nil, codersdk.ReadBodyAsError(res)
}
if res != nil && res.Body != nil {
defer res.Body.Close()
}
d := wsjson.NewDecoder[codersdk.WorkspaceAgentListContainersResponse](conn, websocket.MessageText, logger)
return d.Chan(), d, nil
}
// RecreateDevcontainer recreates a devcontainer with the given container.
// This is a blocking call and will wait for the container to be recreated.
func (c *AgentConn) RecreateDevcontainer(ctx context.Context, devcontainerID string) (codersdk.Response, error) {