Skip to content

Commit 7011e4b

Browse files
stirbyspikecurtismatifalimtojekethanndickson
authored
chore: cherry-pick patches for 2.17.3 (#15852)
Co-authored-by: Spike Curtis <spike@coder.com> Co-authored-by: Muhammad Atif Ali <atif@coder.com> Co-authored-by: Marcin Tojek <mtojek@users.noreply.github.com> Co-authored-by: Ethan <39577870+ethanndickson@users.noreply.github.com>
1 parent dbfadf2 commit 7011e4b

File tree

15 files changed

+186
-84
lines changed

15 files changed

+186
-84
lines changed

coderd/files.go

+5-4
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ import (
2525
)
2626

2727
const (
28-
tarMimeType = "application/x-tar"
29-
zipMimeType = "application/zip"
28+
tarMimeType = "application/x-tar"
29+
zipMimeType = "application/zip"
30+
windowsZipMimeType = "application/x-zip-compressed"
3031

3132
HTTPFileMaxBytes = 10 * (10 << 20)
3233
)
@@ -48,7 +49,7 @@ func (api *API) postFile(rw http.ResponseWriter, r *http.Request) {
4849

4950
contentType := r.Header.Get("Content-Type")
5051
switch contentType {
51-
case tarMimeType, zipMimeType:
52+
case tarMimeType, zipMimeType, windowsZipMimeType:
5253
default:
5354
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
5455
Message: fmt.Sprintf("Unsupported content type header %q.", contentType),
@@ -66,7 +67,7 @@ func (api *API) postFile(rw http.ResponseWriter, r *http.Request) {
6667
return
6768
}
6869

69-
if contentType == zipMimeType {
70+
if contentType == zipMimeType || contentType == windowsZipMimeType {
7071
zipReader, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
7172
if err != nil {
7273
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{

coderd/files_test.go

+12
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,18 @@ func TestPostFiles(t *testing.T) {
4343
require.NoError(t, err)
4444
})
4545

46+
t.Run("InsertWindowsZip", func(t *testing.T) {
47+
t.Parallel()
48+
client := coderdtest.New(t, nil)
49+
_ = coderdtest.CreateFirstUser(t, client)
50+
51+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
52+
defer cancel()
53+
54+
_, err := client.Upload(ctx, "application/x-zip-compressed", bytes.NewReader(archivetest.TestZipFileBytes()))
55+
require.NoError(t, err)
56+
})
57+
4658
t.Run("InsertAlreadyExists", func(t *testing.T) {
4759
t.Parallel()
4860
client := coderdtest.New(t, nil)

coderd/provisionerjobs.go

+4-5
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"nhooyr.io/websocket"
1616

1717
"cdr.dev/slog"
18+
"github.com/coder/coder/v2/codersdk/wsjson"
1819

1920
"github.com/coder/coder/v2/coderd/database"
2021
"github.com/coder/coder/v2/coderd/database/db2sdk"
@@ -312,6 +313,7 @@ type logFollower struct {
312313
r *http.Request
313314
rw http.ResponseWriter
314315
conn *websocket.Conn
316+
enc *wsjson.Encoder[codersdk.ProvisionerJobLog]
315317

316318
jobID uuid.UUID
317319
after int64
@@ -391,6 +393,7 @@ func (f *logFollower) follow() {
391393
}
392394
defer f.conn.Close(websocket.StatusNormalClosure, "done")
393395
go httpapi.Heartbeat(f.ctx, f.conn)
396+
f.enc = wsjson.NewEncoder[codersdk.ProvisionerJobLog](f.conn, websocket.MessageText)
394397

395398
// query for logs once right away, so we can get historical data from before
396399
// subscription
@@ -488,11 +491,7 @@ func (f *logFollower) query() error {
488491
return xerrors.Errorf("error fetching logs: %w", err)
489492
}
490493
for _, log := range logs {
491-
logB, err := json.Marshal(convertProvisionerJobLog(log))
492-
if err != nil {
493-
return xerrors.Errorf("error marshaling log: %w", err)
494-
}
495-
err = f.conn.Write(f.ctx, websocket.MessageText, logB)
494+
err := f.enc.Encode(convertProvisionerJobLog(log))
496495
if err != nil {
497496
return xerrors.Errorf("error writing to websocket: %w", err)
498497
}

coderd/workspaceagents.go

+7-17
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
"github.com/coder/coder/v2/codersdk"
3838
"github.com/coder/coder/v2/codersdk/agentsdk"
3939
"github.com/coder/coder/v2/codersdk/workspacesdk"
40+
"github.com/coder/coder/v2/codersdk/wsjson"
4041
"github.com/coder/coder/v2/tailnet"
4142
"github.com/coder/coder/v2/tailnet/proto"
4243
)
@@ -404,11 +405,9 @@ func (api *API) workspaceAgentLogs(rw http.ResponseWriter, r *http.Request) {
404405
}
405406
go httpapi.Heartbeat(ctx, conn)
406407

407-
ctx, wsNetConn := codersdk.WebsocketNetConn(ctx, conn, websocket.MessageText)
408-
defer wsNetConn.Close() // Also closes conn.
408+
encoder := wsjson.NewEncoder[[]codersdk.WorkspaceAgentLog](conn, websocket.MessageText)
409+
defer encoder.Close(websocket.StatusNormalClosure)
409410

410-
// The Go stdlib JSON encoder appends a newline character after message write.
411-
encoder := json.NewEncoder(wsNetConn)
412411
err = encoder.Encode(convertWorkspaceAgentLogs(logs))
413412
if err != nil {
414413
return
@@ -741,16 +740,8 @@ func (api *API) derpMapUpdates(rw http.ResponseWriter, r *http.Request) {
741740
})
742741
return
743742
}
744-
ctx, nconn := codersdk.WebsocketNetConn(ctx, ws, websocket.MessageBinary)
745-
defer nconn.Close()
746-
747-
// Slurp all packets from the connection into io.Discard so pongs get sent
748-
// by the websocket package. We don't do any reads ourselves so this is
749-
// necessary.
750-
go func() {
751-
_, _ = io.Copy(io.Discard, nconn)
752-
_ = nconn.Close()
753-
}()
743+
encoder := wsjson.NewEncoder[*tailcfg.DERPMap](ws, websocket.MessageBinary)
744+
defer encoder.Close(websocket.StatusGoingAway)
754745

755746
go func(ctx context.Context) {
756747
// TODO(mafredri): Is this too frequent? Use separate ping disconnect timeout?
@@ -768,7 +759,7 @@ func (api *API) derpMapUpdates(rw http.ResponseWriter, r *http.Request) {
768759
err := ws.Ping(ctx)
769760
cancel()
770761
if err != nil {
771-
_ = nconn.Close()
762+
_ = ws.Close(websocket.StatusGoingAway, "ping failed")
772763
return
773764
}
774765
}
@@ -781,9 +772,8 @@ func (api *API) derpMapUpdates(rw http.ResponseWriter, r *http.Request) {
781772
for {
782773
derpMap := api.DERPMap()
783774
if lastDERPMap == nil || !tailnet.CompareDERPMaps(lastDERPMap, derpMap) {
784-
err := json.NewEncoder(nconn).Encode(derpMap)
775+
err := encoder.Encode(derpMap)
785776
if err != nil {
786-
_ = nconn.Close()
787777
return
788778
}
789779
lastDERPMap = derpMap

codersdk/provisionerdaemons.go

+3-30
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919

2020
"github.com/coder/coder/v2/buildinfo"
2121
"github.com/coder/coder/v2/codersdk/drpc"
22+
"github.com/coder/coder/v2/codersdk/wsjson"
2223
"github.com/coder/coder/v2/provisionerd/proto"
2324
"github.com/coder/coder/v2/provisionerd/runner"
2425
)
@@ -145,36 +146,8 @@ func (c *Client) provisionerJobLogsAfter(ctx context.Context, path string, after
145146
}
146147
return nil, nil, ReadBodyAsError(res)
147148
}
148-
logs := make(chan ProvisionerJobLog)
149-
closed := make(chan struct{})
150-
go func() {
151-
defer close(closed)
152-
defer close(logs)
153-
defer conn.Close(websocket.StatusGoingAway, "")
154-
var log ProvisionerJobLog
155-
for {
156-
msgType, msg, err := conn.Read(ctx)
157-
if err != nil {
158-
return
159-
}
160-
if msgType != websocket.MessageText {
161-
return
162-
}
163-
err = json.Unmarshal(msg, &log)
164-
if err != nil {
165-
return
166-
}
167-
select {
168-
case <-ctx.Done():
169-
return
170-
case logs <- log:
171-
}
172-
}
173-
}()
174-
return logs, closeFunc(func() error {
175-
<-closed
176-
return nil
177-
}), nil
149+
d := wsjson.NewDecoder[ProvisionerJobLog](conn, websocket.MessageText, c.logger)
150+
return d.Chan(), d, nil
178151
}
179152

180153
// ServeProvisionerDaemonRequest are the parameters to call ServeProvisionerDaemon with

codersdk/workspaceagents.go

+3-26
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"nhooyr.io/websocket"
1616

1717
"github.com/coder/coder/v2/coderd/tracing"
18+
"github.com/coder/coder/v2/codersdk/wsjson"
1819
)
1920

2021
type WorkspaceAgentStatus string
@@ -454,30 +455,6 @@ func (c *Client) WorkspaceAgentLogsAfter(ctx context.Context, agentID uuid.UUID,
454455
}
455456
return nil, nil, ReadBodyAsError(res)
456457
}
457-
logChunks := make(chan []WorkspaceAgentLog, 1)
458-
closed := make(chan struct{})
459-
ctx, wsNetConn := WebsocketNetConn(ctx, conn, websocket.MessageText)
460-
decoder := json.NewDecoder(wsNetConn)
461-
go func() {
462-
defer close(closed)
463-
defer close(logChunks)
464-
defer conn.Close(websocket.StatusGoingAway, "")
465-
for {
466-
var logs []WorkspaceAgentLog
467-
err = decoder.Decode(&logs)
468-
if err != nil {
469-
return
470-
}
471-
select {
472-
case <-ctx.Done():
473-
return
474-
case logChunks <- logs:
475-
}
476-
}
477-
}()
478-
return logChunks, closeFunc(func() error {
479-
_ = wsNetConn.Close()
480-
<-closed
481-
return nil
482-
}), nil
458+
d := wsjson.NewDecoder[[]WorkspaceAgentLog](conn, websocket.MessageText, c.logger)
459+
return d.Chan(), d, nil
483460
}

codersdk/wsjson/decoder.go

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package wsjson
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"sync/atomic"
7+
8+
"nhooyr.io/websocket"
9+
10+
"cdr.dev/slog"
11+
)
12+
13+
type Decoder[T any] struct {
14+
conn *websocket.Conn
15+
typ websocket.MessageType
16+
ctx context.Context
17+
cancel context.CancelFunc
18+
chanCalled atomic.Bool
19+
logger slog.Logger
20+
}
21+
22+
// Chan starts the decoder reading from the websocket and returns a channel for reading the
23+
// resulting values. The chan T is closed if the underlying websocket is closed, or we encounter an
24+
// error. We also close the underlying websocket if we encounter an error reading or decoding.
25+
func (d *Decoder[T]) Chan() <-chan T {
26+
if !d.chanCalled.CompareAndSwap(false, true) {
27+
panic("chan called more than once")
28+
}
29+
values := make(chan T, 1)
30+
go func() {
31+
defer close(values)
32+
defer d.conn.Close(websocket.StatusGoingAway, "")
33+
for {
34+
// we don't use d.ctx here because it only gets canceled after closing the connection
35+
// and a "connection closed" type error is more clear than context canceled.
36+
typ, b, err := d.conn.Read(context.Background())
37+
if err != nil {
38+
// might be benign like EOF, so just log at debug
39+
d.logger.Debug(d.ctx, "error reading from websocket", slog.Error(err))
40+
return
41+
}
42+
if typ != d.typ {
43+
d.logger.Error(d.ctx, "websocket type mismatch while decoding")
44+
return
45+
}
46+
var value T
47+
err = json.Unmarshal(b, &value)
48+
if err != nil {
49+
d.logger.Error(d.ctx, "error unmarshalling", slog.Error(err))
50+
return
51+
}
52+
select {
53+
case values <- value:
54+
// OK
55+
case <-d.ctx.Done():
56+
return
57+
}
58+
}
59+
}()
60+
return values
61+
}
62+
63+
// nolint: revive // complains that Encoder has the same function name
64+
func (d *Decoder[T]) Close() error {
65+
err := d.conn.Close(websocket.StatusNormalClosure, "")
66+
d.cancel()
67+
return err
68+
}
69+
70+
// NewDecoder creates a JSON-over-websocket decoder for type T, which must be deserializable from
71+
// JSON.
72+
func NewDecoder[T any](conn *websocket.Conn, typ websocket.MessageType, logger slog.Logger) *Decoder[T] {
73+
ctx, cancel := context.WithCancel(context.Background())
74+
return &Decoder[T]{conn: conn, ctx: ctx, cancel: cancel, typ: typ, logger: logger}
75+
}

codersdk/wsjson/encoder.go

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package wsjson
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
7+
"golang.org/x/xerrors"
8+
"nhooyr.io/websocket"
9+
)
10+
11+
type Encoder[T any] struct {
12+
conn *websocket.Conn
13+
typ websocket.MessageType
14+
}
15+
16+
func (e *Encoder[T]) Encode(v T) error {
17+
w, err := e.conn.Writer(context.Background(), e.typ)
18+
if err != nil {
19+
return xerrors.Errorf("get websocket writer: %w", err)
20+
}
21+
defer w.Close()
22+
j := json.NewEncoder(w)
23+
err = j.Encode(v)
24+
if err != nil {
25+
return xerrors.Errorf("encode json: %w", err)
26+
}
27+
return nil
28+
}
29+
30+
func (e *Encoder[T]) Close(c websocket.StatusCode) error {
31+
return e.conn.Close(c, "")
32+
}
33+
34+
// NewEncoder creates a JSON-over websocket encoder for the type T, which must be JSON-serializable.
35+
// You may then call Encode() to send objects over the websocket. Creating an Encoder closes the
36+
// websocket for reading, turning it into a unidirectional write stream of JSON-encoded objects.
37+
func NewEncoder[T any](conn *websocket.Conn, typ websocket.MessageType) *Encoder[T] {
38+
// Here we close the websocket for reading, so that the websocket library will handle pings and
39+
// close frames.
40+
_ = conn.CloseRead(context.Background())
41+
return &Encoder[T]{conn: conn, typ: typ}
42+
}
Loading

docs/user-guides/workspace-access/remote-desktops.md

+9
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,12 @@ requires just a few lines of Terraform in your template, see the documentation
5858
on our registry for setup.
5959

6060
![Web RDP Module in a Workspace](../../images/user-guides/web-rdp-demo.png)
61+
62+
## Amazon DCV Windows
63+
64+
Our [Amazon DCV Windows](https://registry.coder.com/modules/amazon-dcv-windows)
65+
module adds a one-click button to open an Amazon DCV session in the browser.
66+
This requires just a few lines of Terraform in your template, see the
67+
documentation on our registry for setup.
68+
69+
![Amazon DCV Windows Module in a Workspace](../../images/user-guides/amazon-dcv-windows-demo.png)

site/src/api/api.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1868,7 +1868,7 @@ class ApiMethods {
18681868

18691869
uploadFile = async (file: File): Promise<TypesGen.UploadResponse> => {
18701870
const response = await this.axios.post("/api/v2/files", file, {
1871-
headers: { "Content-Type": "application/x-tar" },
1871+
headers: { "Content-Type": file.type },
18721872
});
18731873

18741874
return response.data;

0 commit comments

Comments
 (0)