diff --git a/ci/steps/build.sh b/ci/steps/build.sh index 6307c0d7..64ef65b2 100755 --- a/ci/steps/build.sh +++ b/ci/steps/build.sh @@ -13,7 +13,7 @@ build() { echo "Building coder-cli for $GOOS-$GOARCH..." tmpdir=$(mktemp -d) - go build -ldflags "-X main.version=${tag}" -o "$tmpdir/coder" ../../cmd/coder + go build -ldflags "-X cdr.dev/coder-cli/internal/version.Version=${tag}" -o "$tmpdir/coder" ../../cmd/coder pushd "$tmpdir" if [[ "$GOOS" == "windows" ]]; then diff --git a/cmd/coder/main.go b/cmd/coder/main.go index 8ba4ab19..d59a1abe 100644 --- a/cmd/coder/main.go +++ b/cmd/coder/main.go @@ -11,12 +11,10 @@ import ( "cdr.dev/coder-cli/internal/clog" "cdr.dev/coder-cli/internal/cmd" + "cdr.dev/coder-cli/internal/version" "cdr.dev/coder-cli/internal/x/xterminal" ) -// Using a global for the version so it can be set at build time using ldflags. -var version = "unknown" - func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -39,7 +37,7 @@ func main() { }() app := cmd.Make() - app.Version = fmt.Sprintf("%s %s %s/%s", version, runtime.Version(), runtime.GOOS, runtime.GOARCH) + app.Version = fmt.Sprintf("%s %s %s/%s", version.Version, runtime.Version(), runtime.GOOS, runtime.GOARCH) if err := app.ExecuteContext(ctx); err != nil { clog.Log(err) diff --git a/coder-sdk/version.go b/coder-sdk/version.go new file mode 100644 index 00000000..a2211afe --- /dev/null +++ b/coder-sdk/version.go @@ -0,0 +1,22 @@ +package coder + +import ( + "context" + "net/http" +) + +// APIVersion parses the coder-version http header from an authenticated request. +func (c Client) APIVersion(ctx context.Context) (string, error) { + const coderVersionHeaderKey = "coder-version" + resp, err := c.request(ctx, http.MethodGet, "/api/users/"+Me, nil) + if err != nil { + return "", err + } + + version := resp.Header.Get(coderVersionHeaderKey) + if version == "" { + version = "unknown" + } + + return version, nil +} diff --git a/internal/cmd/auth.go b/internal/cmd/auth.go index b0105ac4..b048593c 100644 --- a/internal/cmd/auth.go +++ b/internal/cmd/auth.go @@ -2,6 +2,7 @@ package cmd import ( "context" + "fmt" "net/http" "net/url" @@ -10,6 +11,7 @@ import ( "cdr.dev/coder-cli/coder-sdk" "cdr.dev/coder-cli/internal/clog" "cdr.dev/coder-cli/internal/config" + "cdr.dev/coder-cli/internal/version" ) var errNeedLogin = clog.Fatal( @@ -18,6 +20,7 @@ var errNeedLogin = clog.Fatal( ) func newClient() (*coder.Client, error) { + ctx := context.Background() sessionToken, err := config.Session.Read() if err != nil { return nil, errNeedLogin @@ -38,9 +41,7 @@ func newClient() (*coder.Client, error) { Token: sessionToken, } - // Make sure we can make a request so the final - // error is more clean. - _, err = c.Me(context.Background()) + apiVersion, err := c.APIVersion(ctx) if err != nil { var he *coder.HTTPError if xerrors.As(err, &he) { @@ -52,5 +53,14 @@ func newClient() (*coder.Client, error) { return nil, err } + if !version.VersionsMatch(apiVersion) { + clog.Log(clog.Warn( + "version mismatch detected", + fmt.Sprintf("coder-cli version: %s", version.Version), + fmt.Sprintf("Coder API version: %s", apiVersion), clog.BlankLine, + clog.Tipf("download the appropriate version here: https://github.com/cdr/coder-cli/releases"), + )) + } + return c, nil } diff --git a/internal/version/version.go b/internal/version/version.go new file mode 100644 index 00000000..00effefa --- /dev/null +++ b/internal/version/version.go @@ -0,0 +1,18 @@ +package version + +import ( + "strings" +) + +// Version is populated at compile-time with the current coder-cli version. +var Version string = "unknown" + +// VersionMatch compares the given APIVersion to the compile-time injected coder-cli version. +func VersionsMatch(apiVersion string) bool { + withoutPatchRelease := strings.Split(Version, ".") + if len(withoutPatchRelease) < 3 { + return false + } + majorMinor := strings.Join(withoutPatchRelease[:len(withoutPatchRelease)-1], ".") + return strings.HasPrefix(strings.TrimPrefix(apiVersion, "v"), strings.TrimPrefix(majorMinor, "v")) +} diff --git a/internal/version/version_test.go b/internal/version/version_test.go new file mode 100644 index 00000000..9791717e --- /dev/null +++ b/internal/version/version_test.go @@ -0,0 +1,29 @@ +package version + +import ( + "testing" + + "cdr.dev/slog/sloggers/slogtest/assert" +) + +func TestVersion(t *testing.T) { + Version = "1.12.1" + match := VersionsMatch("1.12.2") + assert.True(t, "versions match", match) + + Version = "v1.14.1" + match = VersionsMatch("1.15.2") + assert.True(t, "versions do not match", !match) + + Version = "v1.15.4" + match = VersionsMatch("1.15.2") + assert.True(t, "versions do match", match) + + Version = "1.15.4" + match = VersionsMatch("v1.15.2") + assert.True(t, "versions do match", match) + + Version = "1.15.4" + match = VersionsMatch("v2.15.2") + assert.True(t, "versions do not match", !match) +}