mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore: watch workspace endpoint (#4060)
This commit is contained in:
+1
-33
@@ -12,7 +12,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
"nhooyr.io/websocket"
|
||||
)
|
||||
|
||||
// These cookies are Coder-specific. If a new one is added or changed, the name
|
||||
@@ -95,41 +94,10 @@ func (c *Client) Request(ctx context.Context, method, path string, body interfac
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// dialWebsocket opens a dialWebsocket connection on that path provided.
|
||||
// The caller is responsible for closing the dialWebsocket.Conn.
|
||||
func (c *Client) dialWebsocket(ctx context.Context, path string) (*websocket.Conn, error) {
|
||||
serverURL, err := c.URL.Parse(path)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("parse path: %w", err)
|
||||
}
|
||||
|
||||
apiURL, err := url.Parse(serverURL.String())
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("parse server url: %w", err)
|
||||
}
|
||||
apiURL.Scheme = "ws"
|
||||
if serverURL.Scheme == "https" {
|
||||
apiURL.Scheme = "wss"
|
||||
}
|
||||
apiURL.Path = path
|
||||
q := apiURL.Query()
|
||||
q.Add(SessionTokenKey, c.SessionToken)
|
||||
apiURL.RawQuery = q.Encode()
|
||||
|
||||
//nolint:bodyclose
|
||||
conn, _, err := websocket.Dial(ctx, apiURL.String(), &websocket.DialOptions{
|
||||
HTTPClient: c.HTTPClient,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("dial websocket: %w", err)
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// readBodyAsError reads the response as an .Message, and
|
||||
// wraps it in a codersdk.Error type for easy marshaling.
|
||||
func readBodyAsError(res *http.Response) error {
|
||||
defer res.Body.Close()
|
||||
contentType := res.Header.Get("Content-Type")
|
||||
|
||||
var method, u string
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package codersdk
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
type ServerSentEvent struct {
|
||||
Type ServerSentEventType `json:"type"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
type ServerSentEventType string
|
||||
|
||||
const (
|
||||
ServerSentEventTypePing ServerSentEventType = "ping"
|
||||
ServerSentEventTypeData ServerSentEventType = "data"
|
||||
ServerSentEventTypeError ServerSentEventType = "error"
|
||||
)
|
||||
|
||||
func ServerSentEventReader(rc io.ReadCloser) func() (*ServerSentEvent, error) {
|
||||
reader := bufio.NewReader(rc)
|
||||
nextLineValue := func(prefix string) ([]byte, error) {
|
||||
var (
|
||||
line string
|
||||
err error
|
||||
)
|
||||
for {
|
||||
line, err = reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("reading next string: %w", err)
|
||||
}
|
||||
if strings.TrimSpace(line) != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(line, fmt.Sprintf("%s: ", prefix)) {
|
||||
return nil, xerrors.Errorf("expecting %s prefix, got: %s", prefix, line)
|
||||
}
|
||||
s := strings.TrimPrefix(line, fmt.Sprintf("%s: ", prefix))
|
||||
s = strings.TrimSpace(s)
|
||||
return []byte(s), nil
|
||||
}
|
||||
|
||||
nextEvent := func() (*ServerSentEvent, error) {
|
||||
for {
|
||||
t, err := nextLineValue("event")
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("reading next line value: %w", err)
|
||||
}
|
||||
|
||||
switch ServerSentEventType(t) {
|
||||
case ServerSentEventTypePing:
|
||||
return &ServerSentEvent{
|
||||
Type: ServerSentEventTypePing,
|
||||
}, nil
|
||||
case ServerSentEventTypeData:
|
||||
d, err := nextLineValue("data")
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("reading next line value: %w", err)
|
||||
}
|
||||
|
||||
return &ServerSentEvent{
|
||||
Type: ServerSentEventTypeData,
|
||||
Data: d,
|
||||
}, nil
|
||||
case ServerSentEventTypeError:
|
||||
d, err := nextLineValue("data")
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("reading next line value: %w", err)
|
||||
}
|
||||
|
||||
return &ServerSentEvent{
|
||||
Type: ServerSentEventTypeError,
|
||||
Data: d,
|
||||
}, nil
|
||||
default:
|
||||
return nil, xerrors.Errorf("unknown event type: %s", t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nextEvent
|
||||
}
|
||||
@@ -49,8 +49,9 @@ type WorkspaceBuild struct {
|
||||
InitiatorID uuid.UUID `json:"initiator_id"`
|
||||
InitiatorUsername string `json:"initiator_name"`
|
||||
Job ProvisionerJob `json:"job"`
|
||||
Deadline NullTime `json:"deadline,omitempty"`
|
||||
Reason BuildReason `db:"reason" json:"reason"`
|
||||
Resources []WorkspaceResource `json:"resources"`
|
||||
Deadline NullTime `json:"deadline,omitempty"`
|
||||
}
|
||||
|
||||
// WorkspaceBuild returns a single workspace build for a workspace.
|
||||
|
||||
+21
-9
@@ -10,8 +10,6 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
"nhooyr.io/websocket"
|
||||
"nhooyr.io/websocket/wsjson"
|
||||
)
|
||||
|
||||
// Workspace is a deployment of a template. It references a specific
|
||||
@@ -123,28 +121,42 @@ func (c *Client) CreateWorkspaceBuild(ctx context.Context, workspace uuid.UUID,
|
||||
}
|
||||
|
||||
func (c *Client) WatchWorkspace(ctx context.Context, id uuid.UUID) (<-chan Workspace, error) {
|
||||
conn, err := c.dialWebsocket(ctx, fmt.Sprintf("/api/v2/workspaces/%s/watch", id))
|
||||
//nolint:bodyclose
|
||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s/watch", id), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wc := make(chan Workspace, 256)
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, readBodyAsError(res)
|
||||
}
|
||||
nextEvent := ServerSentEventReader(res.Body)
|
||||
|
||||
wc := make(chan Workspace, 256)
|
||||
go func() {
|
||||
defer close(wc)
|
||||
defer conn.Close(websocket.StatusNormalClosure, "")
|
||||
defer res.Body.Close()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
var ws Workspace
|
||||
err := wsjson.Read(ctx, conn, &ws)
|
||||
sse, err := nextEvent()
|
||||
if err != nil {
|
||||
conn.Close(websocket.StatusInternalError, "failed to read workspace")
|
||||
return
|
||||
}
|
||||
wc <- ws
|
||||
if sse.Type == ServerSentEventTypeData {
|
||||
var ws Workspace
|
||||
b, ok := sse.Data.([]byte)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(b, &ws)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
wc <- ws
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
Reference in New Issue
Block a user