Skip to content

Commit 4d2fe26

Browse files
authored
chore(coderd): extract api version validation to util package (#11407)
1 parent 58873fa commit 4d2fe26

File tree

5 files changed

+178
-110
lines changed

5 files changed

+178
-110
lines changed

coderd/util/apiversion/apiversion.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package apiversion
2+
3+
import (
4+
"strconv"
5+
"strings"
6+
7+
"golang.org/x/xerrors"
8+
)
9+
10+
// New returns an *APIVersion with the given major.minor and
11+
// additional supported major versions.
12+
func New(maj, min int) *APIVersion {
13+
v := &APIVersion{
14+
supportedMajor: maj,
15+
supportedMinor: min,
16+
additionalMajors: make([]int, 0),
17+
}
18+
return v
19+
}
20+
21+
type APIVersion struct {
22+
supportedMajor int
23+
supportedMinor int
24+
additionalMajors []int
25+
}
26+
27+
func (v *APIVersion) WithBackwardCompat(majs ...int) *APIVersion {
28+
v.additionalMajors = append(v.additionalMajors, majs[:]...)
29+
return v
30+
}
31+
32+
// Validate validates the given version against the given constraints:
33+
// A given major.minor version is valid iff:
34+
// 1. The requested major version is contained within v.supportedMajors
35+
// 2. If the requested major version is the 'current major', then
36+
// the requested minor version must be less than or equal to the supported
37+
// minor version.
38+
//
39+
// For example, given majors {1, 2} and minor 2, then:
40+
// - 0.x is not supported,
41+
// - 1.x is supported,
42+
// - 2.0, 2.1, and 2.2 are supported,
43+
// - 2.3+ is not supported.
44+
func (v *APIVersion) Validate(version string) error {
45+
major, minor, err := Parse(version)
46+
if err != nil {
47+
return err
48+
}
49+
if major > v.supportedMajor {
50+
return xerrors.Errorf("server is at version %d.%d, behind requested major version %s",
51+
v.supportedMajor, v.supportedMinor, version)
52+
}
53+
if major == v.supportedMajor {
54+
if minor > v.supportedMinor {
55+
return xerrors.Errorf("server is at version %d.%d, behind requested minor version %s",
56+
v.supportedMajor, v.supportedMinor, version)
57+
}
58+
return nil
59+
}
60+
for _, mjr := range v.additionalMajors {
61+
if major == mjr {
62+
return nil
63+
}
64+
}
65+
return xerrors.Errorf("version %s is no longer supported", version)
66+
}
67+
68+
// Parse parses a valid major.minor version string into (major, minor).
69+
// Both major and minor must be valid integers separated by a period '.'.
70+
func Parse(version string) (major int, minor int, err error) {
71+
parts := strings.Split(version, ".")
72+
if len(parts) != 2 {
73+
return 0, 0, xerrors.Errorf("invalid version string: %s", version)
74+
}
75+
major, err = strconv.Atoi(parts[0])
76+
if err != nil {
77+
return 0, 0, xerrors.Errorf("invalid major version: %s", version)
78+
}
79+
minor, err = strconv.Atoi(parts[1])
80+
if err != nil {
81+
return 0, 0, xerrors.Errorf("invalid minor version: %s", version)
82+
}
83+
return major, minor, nil
84+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package apiversion_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
8+
"github.com/coder/coder/v2/coderd/util/apiversion"
9+
)
10+
11+
func TestAPIVersionValidate(t *testing.T) {
12+
t.Parallel()
13+
14+
// Given
15+
v := apiversion.New(2, 1).WithBackwardCompat(1)
16+
17+
for _, tc := range []struct {
18+
name string
19+
version string
20+
expectedError string
21+
}{
22+
{
23+
name: "OK",
24+
version: "2.1",
25+
},
26+
{
27+
name: "MinorOK",
28+
version: "2.0",
29+
},
30+
{
31+
name: "MajorOK",
32+
version: "1.0",
33+
},
34+
{
35+
name: "TooNewMinor",
36+
version: "2.2",
37+
expectedError: "behind requested minor version",
38+
},
39+
{
40+
name: "TooNewMajor",
41+
version: "3.1",
42+
expectedError: "behind requested major version",
43+
},
44+
{
45+
name: "Malformed0",
46+
version: "cats",
47+
expectedError: "invalid version string",
48+
},
49+
{
50+
name: "Malformed1",
51+
version: "cats.dogs",
52+
expectedError: "invalid major version",
53+
},
54+
{
55+
name: "Malformed2",
56+
version: "1.dogs",
57+
expectedError: "invalid minor version",
58+
},
59+
{
60+
name: "Malformed3",
61+
version: "1.0.1",
62+
expectedError: "invalid version string",
63+
},
64+
{
65+
name: "Malformed4",
66+
version: "11",
67+
expectedError: "invalid version string",
68+
},
69+
{
70+
name: "TooOld",
71+
version: "0.8",
72+
expectedError: "no longer supported",
73+
},
74+
} {
75+
tc := tc
76+
t.Run(tc.name, func(t *testing.T) {
77+
t.Parallel()
78+
79+
// When
80+
err := v.Validate(tc.version)
81+
82+
// Then
83+
if tc.expectedError == "" {
84+
require.NoError(t, err)
85+
} else {
86+
require.ErrorContains(t, err, tc.expectedError)
87+
}
88+
})
89+
}
90+
}

coderd/workspaceagents.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1180,7 +1180,7 @@ func (api *API) workspaceAgentClientCoordinate(rw http.ResponseWriter, r *http.R
11801180
if qv != "" {
11811181
version = qv
11821182
}
1183-
if err := tailnet.ValidateVersion(version); err != nil {
1183+
if err := tailnet.CurrentVersion.Validate(version); err != nil {
11841184
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
11851185
Message: "Unknown or unsupported API version",
11861186
Validations: []codersdk.ValidationError{

tailnet/service.go

Lines changed: 3 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import (
44
"context"
55
"io"
66
"net"
7-
"strconv"
8-
"strings"
97
"sync/atomic"
108
"time"
119

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

1816
"cdr.dev/slog"
17+
"github.com/coder/coder/v2/coderd/util/apiversion"
1918
"github.com/coder/coder/v2/tailnet/proto"
2019

2120
"golang.org/x/xerrors"
@@ -26,47 +25,7 @@ const (
2625
CurrentMinor = 0
2726
)
2827

29-
var SupportedMajors = []int{2, 1}
30-
31-
func ValidateVersion(version string) error {
32-
major, minor, err := parseVersion(version)
33-
if err != nil {
34-
return err
35-
}
36-
if major > CurrentMajor {
37-
return xerrors.Errorf("server is at version %d.%d, behind requested version %s",
38-
CurrentMajor, CurrentMinor, version)
39-
}
40-
if major == CurrentMajor {
41-
if minor > CurrentMinor {
42-
return xerrors.Errorf("server is at version %d.%d, behind requested version %s",
43-
CurrentMajor, CurrentMinor, version)
44-
}
45-
return nil
46-
}
47-
for _, mjr := range SupportedMajors {
48-
if major == mjr {
49-
return nil
50-
}
51-
}
52-
return xerrors.Errorf("version %s is no longer supported", version)
53-
}
54-
55-
func parseVersion(version string) (major int, minor int, err error) {
56-
parts := strings.Split(version, ".")
57-
if len(parts) != 2 {
58-
return 0, 0, xerrors.Errorf("invalid version string: %s", version)
59-
}
60-
major, err = strconv.Atoi(parts[0])
61-
if err != nil {
62-
return 0, 0, xerrors.Errorf("invalid major version: %s", version)
63-
}
64-
minor, err = strconv.Atoi(parts[1])
65-
if err != nil {
66-
return 0, 0, xerrors.Errorf("invalid minor version: %s", version)
67-
}
68-
return major, minor, nil
69-
}
28+
var CurrentVersion = apiversion.New(CurrentMajor, CurrentMinor).WithBackwardCompat(1)
7029

7130
type streamIDContextKey struct{}
7231

@@ -127,7 +86,7 @@ func NewClientService(
12786
}
12887

12988
func (s *ClientService) ServeClient(ctx context.Context, version string, conn net.Conn, id uuid.UUID, agent uuid.UUID) error {
130-
major, _, err := parseVersion(version)
89+
major, _, err := apiversion.Parse(version)
13190
if err != nil {
13291
s.logger.Warn(ctx, "serve client called with unparsable version", slog.Error(err))
13392
return err

tailnet/service_test.go

Lines changed: 0 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package tailnet_test
22

33
import (
44
"context"
5-
"fmt"
65
"io"
76
"net"
87
"net/http"
@@ -25,70 +24,6 @@ import (
2524
"github.com/coder/coder/v2/tailnet"
2625
)
2726

28-
func TestValidateVersion(t *testing.T) {
29-
t.Parallel()
30-
for _, tc := range []struct {
31-
name string
32-
version string
33-
supported bool
34-
}{
35-
{
36-
name: "Current",
37-
version: fmt.Sprintf("%d.%d", tailnet.CurrentMajor, tailnet.CurrentMinor),
38-
supported: true,
39-
},
40-
{
41-
name: "TooNewMinor",
42-
version: fmt.Sprintf("%d.%d", tailnet.CurrentMajor, tailnet.CurrentMinor+1),
43-
},
44-
{
45-
name: "TooNewMajor",
46-
version: fmt.Sprintf("%d.%d", tailnet.CurrentMajor+1, tailnet.CurrentMinor),
47-
},
48-
{
49-
name: "1.0",
50-
version: "1.0",
51-
supported: true,
52-
},
53-
{
54-
name: "2.0",
55-
version: "2.0",
56-
supported: true,
57-
},
58-
{
59-
name: "Malformed0",
60-
version: "cats",
61-
},
62-
{
63-
name: "Malformed1",
64-
version: "cats.dogs",
65-
},
66-
{
67-
name: "Malformed2",
68-
version: "1.0.1",
69-
},
70-
{
71-
name: "Malformed3",
72-
version: "11",
73-
},
74-
{
75-
name: "TooOld",
76-
version: "0.8",
77-
},
78-
} {
79-
tc := tc
80-
t.Run(tc.name, func(t *testing.T) {
81-
t.Parallel()
82-
err := tailnet.ValidateVersion(tc.version)
83-
if tc.supported {
84-
require.NoError(t, err)
85-
} else {
86-
require.Error(t, err)
87-
}
88-
})
89-
}
90-
}
91-
9227
func TestClientService_ServeClient_V2(t *testing.T) {
9328
t.Parallel()
9429
fCoord := newFakeCoordinator()

0 commit comments

Comments
 (0)