Thanks to visit codestin.com
Credit goes to github.com

Skip to content

chore: cherry-pick patches for 2.17.3 #15852

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions coderd/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ import (
)

const (
tarMimeType = "application/x-tar"
zipMimeType = "application/zip"
tarMimeType = "application/x-tar"
zipMimeType = "application/zip"
windowsZipMimeType = "application/x-zip-compressed"

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

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

if contentType == zipMimeType {
if contentType == zipMimeType || contentType == windowsZipMimeType {
zipReader, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Expand Down
12 changes: 12 additions & 0 deletions coderd/files_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ func TestPostFiles(t *testing.T) {
require.NoError(t, err)
})

t.Run("InsertWindowsZip", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)

ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()

_, err := client.Upload(ctx, "application/x-zip-compressed", bytes.NewReader(archivetest.TestZipFileBytes()))
require.NoError(t, err)
})

t.Run("InsertAlreadyExists", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
Expand Down
9 changes: 4 additions & 5 deletions coderd/provisionerjobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"nhooyr.io/websocket"

"cdr.dev/slog"
"github.com/coder/coder/v2/codersdk/wsjson"

"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/db2sdk"
Expand Down Expand Up @@ -312,6 +313,7 @@ type logFollower struct {
r *http.Request
rw http.ResponseWriter
conn *websocket.Conn
enc *wsjson.Encoder[codersdk.ProvisionerJobLog]

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

// query for logs once right away, so we can get historical data from before
// subscription
Expand Down Expand Up @@ -488,11 +491,7 @@ func (f *logFollower) query() error {
return xerrors.Errorf("error fetching logs: %w", err)
}
for _, log := range logs {
logB, err := json.Marshal(convertProvisionerJobLog(log))
if err != nil {
return xerrors.Errorf("error marshaling log: %w", err)
}
err = f.conn.Write(f.ctx, websocket.MessageText, logB)
err := f.enc.Encode(convertProvisionerJobLog(log))
if err != nil {
return xerrors.Errorf("error writing to websocket: %w", err)
}
Expand Down
24 changes: 7 additions & 17 deletions coderd/workspaceagents.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/agentsdk"
"github.com/coder/coder/v2/codersdk/workspacesdk"
"github.com/coder/coder/v2/codersdk/wsjson"
"github.com/coder/coder/v2/tailnet"
"github.com/coder/coder/v2/tailnet/proto"
)
Expand Down Expand Up @@ -404,11 +405,9 @@ func (api *API) workspaceAgentLogs(rw http.ResponseWriter, r *http.Request) {
}
go httpapi.Heartbeat(ctx, conn)

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

// The Go stdlib JSON encoder appends a newline character after message write.
encoder := json.NewEncoder(wsNetConn)
err = encoder.Encode(convertWorkspaceAgentLogs(logs))
if err != nil {
return
Expand Down Expand Up @@ -741,16 +740,8 @@ func (api *API) derpMapUpdates(rw http.ResponseWriter, r *http.Request) {
})
return
}
ctx, nconn := codersdk.WebsocketNetConn(ctx, ws, websocket.MessageBinary)
defer nconn.Close()

// Slurp all packets from the connection into io.Discard so pongs get sent
// by the websocket package. We don't do any reads ourselves so this is
// necessary.
go func() {
_, _ = io.Copy(io.Discard, nconn)
_ = nconn.Close()
}()
encoder := wsjson.NewEncoder[*tailcfg.DERPMap](ws, websocket.MessageBinary)
defer encoder.Close(websocket.StatusGoingAway)

go func(ctx context.Context) {
// TODO(mafredri): Is this too frequent? Use separate ping disconnect timeout?
Expand All @@ -768,7 +759,7 @@ func (api *API) derpMapUpdates(rw http.ResponseWriter, r *http.Request) {
err := ws.Ping(ctx)
cancel()
if err != nil {
_ = nconn.Close()
_ = ws.Close(websocket.StatusGoingAway, "ping failed")
return
}
}
Expand All @@ -781,9 +772,8 @@ func (api *API) derpMapUpdates(rw http.ResponseWriter, r *http.Request) {
for {
derpMap := api.DERPMap()
if lastDERPMap == nil || !tailnet.CompareDERPMaps(lastDERPMap, derpMap) {
err := json.NewEncoder(nconn).Encode(derpMap)
err := encoder.Encode(derpMap)
if err != nil {
_ = nconn.Close()
return
}
lastDERPMap = derpMap
Expand Down
33 changes: 3 additions & 30 deletions codersdk/provisionerdaemons.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (

"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/codersdk/drpc"
"github.com/coder/coder/v2/codersdk/wsjson"
"github.com/coder/coder/v2/provisionerd/proto"
"github.com/coder/coder/v2/provisionerd/runner"
)
Expand Down Expand Up @@ -145,36 +146,8 @@ func (c *Client) provisionerJobLogsAfter(ctx context.Context, path string, after
}
return nil, nil, ReadBodyAsError(res)
}
logs := make(chan ProvisionerJobLog)
closed := make(chan struct{})
go func() {
defer close(closed)
defer close(logs)
defer conn.Close(websocket.StatusGoingAway, "")
var log ProvisionerJobLog
for {
msgType, msg, err := conn.Read(ctx)
if err != nil {
return
}
if msgType != websocket.MessageText {
return
}
err = json.Unmarshal(msg, &log)
if err != nil {
return
}
select {
case <-ctx.Done():
return
case logs <- log:
}
}
}()
return logs, closeFunc(func() error {
<-closed
return nil
}), nil
d := wsjson.NewDecoder[ProvisionerJobLog](conn, websocket.MessageText, c.logger)
return d.Chan(), d, nil
}

// ServeProvisionerDaemonRequest are the parameters to call ServeProvisionerDaemon with
Expand Down
29 changes: 3 additions & 26 deletions codersdk/workspaceagents.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"nhooyr.io/websocket"

"github.com/coder/coder/v2/coderd/tracing"
"github.com/coder/coder/v2/codersdk/wsjson"
)

type WorkspaceAgentStatus string
Expand Down Expand Up @@ -454,30 +455,6 @@ func (c *Client) WorkspaceAgentLogsAfter(ctx context.Context, agentID uuid.UUID,
}
return nil, nil, ReadBodyAsError(res)
}
logChunks := make(chan []WorkspaceAgentLog, 1)
closed := make(chan struct{})
ctx, wsNetConn := WebsocketNetConn(ctx, conn, websocket.MessageText)
decoder := json.NewDecoder(wsNetConn)
go func() {
defer close(closed)
defer close(logChunks)
defer conn.Close(websocket.StatusGoingAway, "")
for {
var logs []WorkspaceAgentLog
err = decoder.Decode(&logs)
if err != nil {
return
}
select {
case <-ctx.Done():
return
case logChunks <- logs:
}
}
}()
return logChunks, closeFunc(func() error {
_ = wsNetConn.Close()
<-closed
return nil
}), nil
d := wsjson.NewDecoder[[]WorkspaceAgentLog](conn, websocket.MessageText, c.logger)
return d.Chan(), d, nil
}
75 changes: 75 additions & 0 deletions codersdk/wsjson/decoder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package wsjson

import (
"context"
"encoding/json"
"sync/atomic"

"nhooyr.io/websocket"

"cdr.dev/slog"
)

type Decoder[T any] struct {
conn *websocket.Conn
typ websocket.MessageType
ctx context.Context
cancel context.CancelFunc
chanCalled atomic.Bool
logger slog.Logger
}

// Chan starts the decoder reading from the websocket and returns a channel for reading the
// resulting values. The chan T is closed if the underlying websocket is closed, or we encounter an
// error. We also close the underlying websocket if we encounter an error reading or decoding.
func (d *Decoder[T]) Chan() <-chan T {
if !d.chanCalled.CompareAndSwap(false, true) {
panic("chan called more than once")
}
values := make(chan T, 1)
go func() {
defer close(values)
defer d.conn.Close(websocket.StatusGoingAway, "")
for {
// we don't use d.ctx here because it only gets canceled after closing the connection
// and a "connection closed" type error is more clear than context canceled.
typ, b, err := d.conn.Read(context.Background())
if err != nil {
// might be benign like EOF, so just log at debug
d.logger.Debug(d.ctx, "error reading from websocket", slog.Error(err))
return
}
if typ != d.typ {
d.logger.Error(d.ctx, "websocket type mismatch while decoding")
return
}
var value T
err = json.Unmarshal(b, &value)
if err != nil {
d.logger.Error(d.ctx, "error unmarshalling", slog.Error(err))
return
}
select {
case values <- value:
// OK
case <-d.ctx.Done():
return
}
}
}()
return values
}

// nolint: revive // complains that Encoder has the same function name
func (d *Decoder[T]) Close() error {
err := d.conn.Close(websocket.StatusNormalClosure, "")
d.cancel()
return err
}

// NewDecoder creates a JSON-over-websocket decoder for type T, which must be deserializable from
// JSON.
func NewDecoder[T any](conn *websocket.Conn, typ websocket.MessageType, logger slog.Logger) *Decoder[T] {
ctx, cancel := context.WithCancel(context.Background())
return &Decoder[T]{conn: conn, ctx: ctx, cancel: cancel, typ: typ, logger: logger}
}
42 changes: 42 additions & 0 deletions codersdk/wsjson/encoder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package wsjson

import (
"context"
"encoding/json"

"golang.org/x/xerrors"
"nhooyr.io/websocket"
)

type Encoder[T any] struct {
conn *websocket.Conn
typ websocket.MessageType
}

func (e *Encoder[T]) Encode(v T) error {
w, err := e.conn.Writer(context.Background(), e.typ)
if err != nil {
return xerrors.Errorf("get websocket writer: %w", err)
}
defer w.Close()
j := json.NewEncoder(w)
err = j.Encode(v)
if err != nil {
return xerrors.Errorf("encode json: %w", err)
}
return nil
}

func (e *Encoder[T]) Close(c websocket.StatusCode) error {
return e.conn.Close(c, "")
}

// NewEncoder creates a JSON-over websocket encoder for the type T, which must be JSON-serializable.
// You may then call Encode() to send objects over the websocket. Creating an Encoder closes the
// websocket for reading, turning it into a unidirectional write stream of JSON-encoded objects.
func NewEncoder[T any](conn *websocket.Conn, typ websocket.MessageType) *Encoder[T] {
// Here we close the websocket for reading, so that the websocket library will handle pings and
// close frames.
_ = conn.CloseRead(context.Background())
return &Encoder[T]{conn: conn, typ: typ}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions docs/user-guides/workspace-access/remote-desktops.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,12 @@ requires just a few lines of Terraform in your template, see the documentation
on our registry for setup.

![Web RDP Module in a Workspace](../../images/user-guides/web-rdp-demo.png)

## Amazon DCV Windows

Our [Amazon DCV Windows](https://registry.coder.com/modules/amazon-dcv-windows)
module adds a one-click button to open an Amazon DCV session in the browser.
This requires just a few lines of Terraform in your template, see the
documentation on our registry for setup.

![Amazon DCV Windows Module in a Workspace](../../images/user-guides/amazon-dcv-windows-demo.png)
2 changes: 1 addition & 1 deletion site/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1868,7 +1868,7 @@ class ApiMethods {

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

return response.data;
Expand Down
Loading
Loading