Skip to content

Commit 5b59f28

Browse files
authored
fix: fix workspacesdk to return error on API mismatch (coder#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)