Skip to content

Commit 5b59f28

Browse files
authored
fix: fix workspacesdk to return error on API mismatch (#13683)
1 parent c4f1676 commit 5b59f28

File tree

4 files changed

+66
-3
lines changed

4 files changed

+66
-3
lines changed

codersdk/workspacesdk/connector.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ package workspacesdk
33
import (
44
"context"
55
"errors"
6+
"fmt"
67
"io"
78
"net/http"
9+
"slices"
810
"sync"
911
"time"
1012

@@ -14,6 +16,7 @@ import (
1416
"tailscale.com/tailcfg"
1517

1618
"cdr.dev/slog"
19+
"github.com/coder/coder/v2/buildinfo"
1720
"github.com/coder/coder/v2/codersdk"
1821
"github.com/coder/coder/v2/tailnet"
1922
"github.com/coder/coder/v2/tailnet/proto"
@@ -101,6 +104,9 @@ func (tac *tailnetAPIConnector) run() {
101104
defer close(tac.closed)
102105
for retrier := retry.New(50*time.Millisecond, 10*time.Second); retrier.Wait(tac.ctx); {
103106
tailnetClient, err := tac.dial()
107+
if xerrors.Is(err, &codersdk.Error{}) {
108+
return
109+
}
104110
if err != nil {
105111
continue
106112
}
@@ -110,13 +116,29 @@ func (tac *tailnetAPIConnector) run() {
110116
}
111117
}
112118

119+
var permanentErrorStatuses = []int{
120+
http.StatusConflict, // returned if client/agent connections disabled (browser only)
121+
http.StatusBadRequest, // returned if API mismatch
122+
http.StatusNotFound, // returned if user doesn't have permission or agent doesn't exist
123+
}
124+
113125
func (tac *tailnetAPIConnector) dial() (proto.DRPCTailnetClient, error) {
114126
tac.logger.Debug(tac.ctx, "dialing Coder tailnet v2+ API")
115127
// nolint:bodyclose
116128
ws, res, err := websocket.Dial(tac.ctx, tac.coordinateURL, tac.dialOptions)
117129
if tac.isFirst {
118-
if res != nil && res.StatusCode == http.StatusConflict {
130+
if res != nil && slices.Contains(permanentErrorStatuses, res.StatusCode) {
119131
err = codersdk.ReadBodyAsError(res)
132+
// A bit more human-readable help in the case the API version was rejected
133+
var sdkErr *codersdk.Error
134+
if xerrors.As(err, &sdkErr) {
135+
if sdkErr.Message == AgentAPIMismatchMessage &&
136+
sdkErr.StatusCode() == http.StatusBadRequest {
137+
sdkErr.Helper = fmt.Sprintf(
138+
"Ensure your client release version (%s, different than the API version) matches the server release version",
139+
buildinfo.Version())
140+
}
141+
}
120142
tac.connected <- err
121143
return nil, err
122144
}

codersdk/workspacesdk/connector_internal_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import (
1818

1919
"cdr.dev/slog"
2020
"cdr.dev/slog/sloggers/slogtest"
21+
"github.com/coder/coder/v2/apiversion"
22+
"github.com/coder/coder/v2/coderd/httpapi"
2123
"github.com/coder/coder/v2/codersdk"
2224
"github.com/coder/coder/v2/tailnet"
2325
"github.com/coder/coder/v2/tailnet/proto"
@@ -97,6 +99,41 @@ func TestTailnetAPIConnector_Disconnects(t *testing.T) {
9799
require.NotNil(t, reqDisc.Disconnect)
98100
}
99101

102+
func TestTailnetAPIConnector_UplevelVersion(t *testing.T) {
103+
t.Parallel()
104+
ctx := testutil.Context(t, testutil.WaitShort)
105+
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
106+
agentID := uuid.UUID{0x55}
107+
108+
svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
109+
sVer := apiversion.New(proto.CurrentMajor, proto.CurrentMinor-1)
110+
111+
// the following matches what Coderd does;
112+
// c.f. coderd/workspaceagents.go: workspaceAgentClientCoordinate
113+
cVer := r.URL.Query().Get("version")
114+
if err := sVer.Validate(cVer); err != nil {
115+
httpapi.Write(ctx, w, http.StatusBadRequest, codersdk.Response{
116+
Message: AgentAPIMismatchMessage,
117+
Validations: []codersdk.ValidationError{
118+
{Field: "version", Detail: err.Error()},
119+
},
120+
})
121+
return
122+
}
123+
}))
124+
125+
fConn := newFakeTailnetConn()
126+
127+
uut := runTailnetAPIConnector(ctx, logger, agentID, svr.URL, &websocket.DialOptions{}, fConn)
128+
129+
err := testutil.RequireRecvCtx(ctx, t, uut.connected)
130+
var sdkErr *codersdk.Error
131+
require.ErrorAs(t, err, &sdkErr)
132+
require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
133+
require.Equal(t, AgentAPIMismatchMessage, sdkErr.Message)
134+
require.NotEmpty(t, sdkErr.Helper)
135+
}
136+
100137
type fakeTailnetConn struct{}
101138

102139
func (*fakeTailnetConn) UpdatePeers([]*proto.CoordinateResponse_PeerUpdate) error {

codersdk/workspacesdk/workspacesdk.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ const (
5555
AgentMinimumListeningPort = 9
5656
)
5757

58+
const AgentAPIMismatchMessage = "Unknown or unsupported API version"
59+
5860
// AgentIgnoredListeningPorts contains a list of ports to ignore when looking for
5961
// running applications inside a workspace. We want to ignore non-HTTP servers,
6062
// so we pre-populate this list with common ports that are not HTTP servers.

enterprise/coderd/workspaceagents_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,9 @@ func TestBlockNonBrowser(t *testing.T) {
4646
},
4747
})
4848
r := setupWorkspaceAgent(t, client, user, 0)
49+
ctx := testutil.Context(t, testutil.WaitShort)
4950
//nolint:gocritic // Testing that even the owner gets blocked.
50-
_, err := workspacesdk.New(client).DialAgent(context.Background(), r.sdkAgent.ID, nil)
51+
_, err := workspacesdk.New(client).DialAgent(ctx, r.sdkAgent.ID, nil)
5152
var apiErr *codersdk.Error
5253
require.ErrorAs(t, err, &apiErr)
5354
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
@@ -65,8 +66,9 @@ func TestBlockNonBrowser(t *testing.T) {
6566
},
6667
})
6768
r := setupWorkspaceAgent(t, client, user, 0)
69+
ctx := testutil.Context(t, testutil.WaitShort)
6870
//nolint:gocritic // Testing RBAC is not the point of this test.
69-
conn, err := workspacesdk.New(client).DialAgent(context.Background(), r.sdkAgent.ID, nil)
71+
conn, err := workspacesdk.New(client).DialAgent(ctx, r.sdkAgent.ID, nil)
7072
require.NoError(t, err)
7173
_ = conn.Close()
7274
})

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy