chore: watch workspace endpoint (#4060)

This commit is contained in:
Garrett Delfosse
2022-09-16 18:54:23 +00:00
committed by GitHub
parent b340634aaa
commit 63fd4945a2
14 changed files with 691 additions and 348 deletions
+1 -33
View File
@@ -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
+89
View File
@@ -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
}
+2 -1
View File
@@ -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
View File
@@ -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
}
}
}
}()