From fe44291a2f11acb00fc01ea21b837b7e8a020fa4 Mon Sep 17 00:00:00 2001 From: sreya Date: Fri, 24 Jun 2022 18:20:45 +0000 Subject: [PATCH 01/17] feat: add version checking to CLI --- buildinfo/buildinfo.go | 36 ++++++++++++++++++- buildinfo/buildinfo_test.go | 70 +++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/buildinfo/buildinfo.go b/buildinfo/buildinfo.go index 224a248a8fb5f..dfd0e77c232f8 100644 --- a/buildinfo/buildinfo.go +++ b/buildinfo/buildinfo.go @@ -3,6 +3,7 @@ package buildinfo import ( "fmt" "runtime/debug" + "strings" "sync" "time" @@ -24,6 +25,11 @@ var ( tag string ) +const ( + // develPrefix is prefixed to developer versions of the application. + develPrefix = "v0.0.0-devel" +) + // Version returns the semantic version of the build. // Use golang.org/x/mod/semver to compare versions. func Version() string { @@ -35,7 +41,7 @@ func Version() string { if tag == "" { // This occurs when the tag hasn't been injected, // like when using "go run". - version = "v0.0.0-devel" + revision + version = develPrefix + revision return } version = "v" + tag @@ -48,6 +54,34 @@ func Version() string { return version } +// VersionsMatch compares the two versions. It assumes the versions match if +// the major and the minor versions are equivalent. Patch versions are +// disregarded. If it detects that either version is a developer build it +// returns true. +func VersionsMatch(v1, v2 string) bool { + // Developer versions are disregarded...hopefully they know what they are + // doing. + if strings.HasPrefix(v1, develPrefix) || strings.HasPrefix(v2, develPrefix) { + return true + } + + v1Toks := strings.Split(v1, ".") + v2Toks := strings.Split(v2, ".") + + // Versions should be formatted as "..". + // We assume malformed versions are evidence of a bug and return false. + if len(v1Toks) < 3 || len(v2Toks) < 3 { + return false + } + + // Slice off the patch suffix. Patch versions should be non-breaking + // changes. + v1MajorMinor := strings.Join(v1Toks[:2], ".") + v2MajorMinor := strings.Join(v2Toks[:2], ".") + + return v1MajorMinor == v2MajorMinor +} + // ExternalURL returns a URL referencing the current Coder version. // For production builds, this will link directly to a release. // For development builds, this will link to a commit. diff --git a/buildinfo/buildinfo_test.go b/buildinfo/buildinfo_test.go index 9a92927a5b420..e671476bea583 100644 --- a/buildinfo/buildinfo_test.go +++ b/buildinfo/buildinfo_test.go @@ -1,6 +1,7 @@ package buildinfo_test import ( + "fmt" "testing" "github.com/stretchr/testify/require" @@ -29,4 +30,73 @@ func TestBuildInfo(t *testing.T) { _, valid := buildinfo.Time() require.False(t, valid) }) + + t.Run("VersionsMatch", func(t *testing.T) { + t.Parallel() + + type testcase struct { + name string + v1 string + v2 string + expectMatch bool + } + + cases := []testcase{ + { + name: "OK", + v1: "v1.2.3", + v2: "v1.2.3", + expectMatch: true, + }, + // Test that we return false if a version is malformed. + { + name: "MalformedIgnored", + v1: "v1.2.3", + v2: "v1.2", + expectMatch: false, + }, + // Test that we return true if a developer version is detected. + // Developers do not need to be warned of mismatched versions. + { + name: "DevelIgnored", + v1: "v0.0.0-devel+123abac", + v2: "v1.2.3", + expectMatch: true, + }, + { + name: "MajorMismatch", + v1: "v1.2.3", + v2: "v0.1.2", + expectMatch: false, + }, + { + name: "MinorMismatch", + v1: "v1.2.3", + v2: "v1.3.2", + expectMatch: false, + }, + // Different patches are ok, breaking changes are not allowed + // in patches. + { + name: "PatchMismatch", + v1: "v1.2.3+hash.whocares", + v2: "v1.2.4+somestuff.hm.ok", + expectMatch: true, + }, + } + + for _, c := range cases { + // It's very important to do this since we're running the tests + // in parallel. Otherwise you will likely get the last element + // in the list since the goroutines will likely start executing + // after the for loop has completed. + c := c + t.Run(c.name, func(t *testing.T) { + t.Parallel() + require.Equal(t, c.expectMatch, buildinfo.VersionsMatch(c.v1, c.v2), + fmt.Sprintf("expected match=%v for version %s and %s", c.expectMatch, c.v1, c.v2), + ) + }) + } + }) } From a9b4796c76bf4cd957a0cfbc96186814ceaa0448 Mon Sep 17 00:00:00 2001 From: sreya Date: Fri, 24 Jun 2022 18:40:07 +0000 Subject: [PATCH 02/17] stuff --- cli/root.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cli/root.go b/cli/root.go index 451c2a3e084f2..99a9a49ecf290 100644 --- a/cli/root.go +++ b/cli/root.go @@ -57,6 +57,9 @@ func Root() *cobra.Command { SilenceUsage: true, Long: `Coder — A tool for provisioning self-hosted development environments. `, + PersistentPreRun: func(cmd *cobra.Command, _ []string) { + + }, Example: ` Start a Coder server. ` + cliui.Styles.Code.Render("$ coder server") + ` From 19224b050d56c90a4ad5a201add9d47c4137e916 Mon Sep 17 00:00:00 2001 From: sreya Date: Fri, 24 Jun 2022 21:46:33 +0000 Subject: [PATCH 03/17] add more plumbing --- buildinfo/buildinfo.go | 2 +- cli/login.go | 9 ++++++++ cli/root.go | 52 +++++++++++++++++++++++++++++++++++++++--- codersdk/buildinfo.go | 13 +++++++++++ 4 files changed, 72 insertions(+), 4 deletions(-) diff --git a/buildinfo/buildinfo.go b/buildinfo/buildinfo.go index dfd0e77c232f8..ddddc0ff90caf 100644 --- a/buildinfo/buildinfo.go +++ b/buildinfo/buildinfo.go @@ -51,7 +51,7 @@ func Version() string { version += revision } }) - return version + return "v1.5.0" } // VersionsMatch compares the two versions. It assumes the versions match if diff --git a/cli/login.go b/cli/login.go index 9ae7f8130cd50..6ff76992596cb 100644 --- a/cli/login.go +++ b/cli/login.go @@ -67,6 +67,15 @@ func login() *cobra.Command { } client := codersdk.New(serverURL) + + // Try to check the version of the server prior to logging in. + // It may be useful to warn the user if they are trying to login + // on a very old client. + err = checkVersions(cmd, client) + if err != nil { + return xerrors.Errorf("check versions: %w", err) + } + hasInitialUser, err := client.HasFirstUser(cmd.Context()) if err != nil { return xerrors.Errorf("has initial user: %w", err) diff --git a/cli/root.go b/cli/root.go index 99a9a49ecf290..c1247a1db94d8 100644 --- a/cli/root.go +++ b/cli/root.go @@ -9,6 +9,7 @@ import ( "golang.org/x/xerrors" + "github.com/charmbracelet/lipgloss" "github.com/kirsle/configdir" "github.com/mattn/go-isatty" "github.com/spf13/cobra" @@ -39,6 +40,13 @@ const ( varNoOpen = "no-open" varForceTty = "force-tty" notLoggedInMessage = "You are not logged in. Try logging in using 'coder login '." + + envNoVersionCheck = "CODER_NO_VERSION_WARNING" +) + +var ( + errUnauthenticated = xerrors.New(notLoggedInMessage) + varNoVersionCheck = false ) func init() { @@ -57,9 +65,23 @@ func Root() *cobra.Command { SilenceUsage: true, Long: `Coder — A tool for provisioning self-hosted development environments. `, - PersistentPreRun: func(cmd *cobra.Command, _ []string) { + PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { + if varNoVersionCheck { + return nil + } + client, err := createClient(cmd) + // If the client is unauthenticated we can ignore the check. + // The child commands should handle an unauthenticated client. + if xerrors.Is(err, errUnauthenticated) { + return nil + } + if err != nil { + return xerrors.Errorf("create client: %w", err) + } + return checkVersions(cmd, client) }, + Example: ` Start a Coder server. ` + cliui.Styles.Code.Render("$ coder server") + ` @@ -99,6 +121,7 @@ func Root() *cobra.Command { cmd.PersistentFlags().String(varURL, "", "Specify the URL to your deployment.") cmd.PersistentFlags().String(varToken, "", "Specify an authentication token.") + cliflag.BoolVarP(cmd.PersistentFlags(), &varNoVersionCheck, "no-version-warning", "", envNoVersionCheck, false, "Suppress warning when client and server versions do not match.") cliflag.String(cmd.PersistentFlags(), varAgentToken, "", "CODER_AGENT_TOKEN", "", "Specify an agent authentication token.") _ = cmd.PersistentFlags().MarkHidden(varAgentToken) cliflag.String(cmd.PersistentFlags(), varAgentURL, "", "CODER_AGENT_URL", "", "Specify the URL for an agent to access your deployment.") @@ -143,7 +166,7 @@ func createClient(cmd *cobra.Command) (*codersdk.Client, error) { if err != nil { // If the configuration files are absent, the user is logged out if os.IsNotExist(err) { - return nil, xerrors.New(notLoggedInMessage) + return nil, errUnauthenticated } return nil, err } @@ -158,7 +181,7 @@ func createClient(cmd *cobra.Command) (*codersdk.Client, error) { if err != nil { // If the configuration files are absent, the user is logged out if os.IsNotExist(err) { - return nil, xerrors.New(notLoggedInMessage) + return nil, errUnauthenticated } return nil, err } @@ -332,3 +355,26 @@ func FormatCobraError(err error, cmd *cobra.Command) string { helpErrMsg := fmt.Sprintf("Run '%s --help' for usage.", cmd.CommandPath()) return cliui.Styles.Error.Render(err.Error() + "\n" + helpErrMsg) } + +func checkVersions(cmd *cobra.Command, client *codersdk.Client) error { + clientVersion := buildinfo.Version() + + info, err := client.BuildInfo(cmd.Context()) + if err != nil { + return xerrors.Errorf("build info: %w", err) + } + + if !buildinfo.VersionsMatch(clientVersion, info.Version) { + warn := cliui.Styles.Warn.Copy().Align(lipgloss.Left) + _, _ = fmt.Fprintf(cmd.OutOrStdout(), warn.Render("client/server versions do not match")) + fmt.Println() + _, _ = fmt.Fprintf(cmd.OutOrStdout(), warn.Render("client version: %s"), clientVersion) + fmt.Println() + _, _ = fmt.Fprintf(cmd.OutOrStdout(), warn.Render("server version: %s"), info.Version) + fmt.Println() + _, _ = fmt.Fprintf(cmd.OutOrStdout(), warn.Render("download the appropriate version from https://github.com/coder/coder/releases/tag/%s"), info.TrimmedVersion()) + fmt.Println() + } + + return nil +} diff --git a/codersdk/buildinfo.go b/codersdk/buildinfo.go index 0233047caf98c..5906fac527ebc 100644 --- a/codersdk/buildinfo.go +++ b/codersdk/buildinfo.go @@ -1,6 +1,7 @@ package codersdk import ( + "bytes" "context" "encoding/json" "net/http" @@ -16,6 +17,18 @@ type BuildInfoResponse struct { Version string `json:"version"` } +// TrimmedVersion trims build information from the version. +// E.g. 'v0.7.4-devel+11573034' -> 'v0.7.4'. +func (b BuildInfoResponse) TrimmedVersion() string { + // Linter doesn't like strings.Index... + idx := bytes.Index([]byte(b.Version), []byte("-devel")) + if idx < 0 { + return string(b.Version) + } + + return b.Version[:idx] +} + // BuildInfo returns build information for this instance of Coder. func (c *Client) BuildInfo(ctx context.Context) (BuildInfoResponse, error) { res, err := c.Request(ctx, http.MethodGet, "/api/v2/buildinfo", nil) From ff7d39e5cfc0ee23d0d0bceed5a45b6b248b1c9c Mon Sep 17 00:00:00 2001 From: sreya Date: Fri, 24 Jun 2022 22:06:04 +0000 Subject: [PATCH 04/17] remove some test code --- buildinfo/buildinfo.go | 2 +- cli/root.go | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/buildinfo/buildinfo.go b/buildinfo/buildinfo.go index ddddc0ff90caf..dfd0e77c232f8 100644 --- a/buildinfo/buildinfo.go +++ b/buildinfo/buildinfo.go @@ -51,7 +51,7 @@ func Version() string { version += revision } }) - return "v1.5.0" + return version } // VersionsMatch compares the two versions. It assumes the versions match if diff --git a/cli/root.go b/cli/root.go index c1247a1db94d8..ca2acc7160708 100644 --- a/cli/root.go +++ b/cli/root.go @@ -46,7 +46,7 @@ const ( var ( errUnauthenticated = xerrors.New(notLoggedInMessage) - varNoVersionCheck = false + varSuppressVersion = false ) func init() { @@ -66,7 +66,7 @@ func Root() *cobra.Command { Long: `Coder — A tool for provisioning self-hosted development environments. `, PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { - if varNoVersionCheck { + if varSuppressVersion { return nil } @@ -121,7 +121,7 @@ func Root() *cobra.Command { cmd.PersistentFlags().String(varURL, "", "Specify the URL to your deployment.") cmd.PersistentFlags().String(varToken, "", "Specify an authentication token.") - cliflag.BoolVarP(cmd.PersistentFlags(), &varNoVersionCheck, "no-version-warning", "", envNoVersionCheck, false, "Suppress warning when client and server versions do not match.") + cliflag.BoolVarP(cmd.PersistentFlags(), &varSuppressVersion, "no-version-warning", "", envNoVersionCheck, false, "Suppress warning when client and server versions do not match.") cliflag.String(cmd.PersistentFlags(), varAgentToken, "", "CODER_AGENT_TOKEN", "", "Specify an agent authentication token.") _ = cmd.PersistentFlags().MarkHidden(varAgentToken) cliflag.String(cmd.PersistentFlags(), varAgentURL, "", "CODER_AGENT_URL", "", "Specify the URL for an agent to access your deployment.") @@ -364,16 +364,16 @@ func checkVersions(cmd *cobra.Command, client *codersdk.Client) error { return xerrors.Errorf("build info: %w", err) } + fmtWarningText := `client/server versions do not match +client version: %s +server version: %s +download the appropriate version from https://github.com/coder/coder/releases/tag/%s +` + if !buildinfo.VersionsMatch(clientVersion, info.Version) { warn := cliui.Styles.Warn.Copy().Align(lipgloss.Left) - _, _ = fmt.Fprintf(cmd.OutOrStdout(), warn.Render("client/server versions do not match")) - fmt.Println() - _, _ = fmt.Fprintf(cmd.OutOrStdout(), warn.Render("client version: %s"), clientVersion) - fmt.Println() - _, _ = fmt.Fprintf(cmd.OutOrStdout(), warn.Render("server version: %s"), info.Version) - fmt.Println() - _, _ = fmt.Fprintf(cmd.OutOrStdout(), warn.Render("download the appropriate version from https://github.com/coder/coder/releases/tag/%s"), info.TrimmedVersion()) - fmt.Println() + _, _ = fmt.Fprintf(cmd.OutOrStdout(), warn.Render(fmtWarningText), clientVersion, info.Version, info.TrimmedVersion()) + _, _ = fmt.Fprintln(cmd.OutOrStdout()) } return nil From aa1ee2f5463ce267372971c4dcec8bf82c75dd3a Mon Sep 17 00:00:00 2001 From: sreya Date: Fri, 24 Jun 2022 22:08:51 +0000 Subject: [PATCH 05/17] remove unnecessary type cast --- codersdk/buildinfo.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codersdk/buildinfo.go b/codersdk/buildinfo.go index 5906fac527ebc..b152103e20e0f 100644 --- a/codersdk/buildinfo.go +++ b/codersdk/buildinfo.go @@ -23,7 +23,7 @@ func (b BuildInfoResponse) TrimmedVersion() string { // Linter doesn't like strings.Index... idx := bytes.Index([]byte(b.Version), []byte("-devel")) if idx < 0 { - return string(b.Version) + return b.Version } return b.Version[:idx] From 2b521694201c4ca15f38a27f892727b894165384 Mon Sep 17 00:00:00 2001 From: sreya Date: Fri, 24 Jun 2022 22:17:16 +0000 Subject: [PATCH 06/17] prevent duplicates --- cli/root.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/cli/root.go b/cli/root.go index ca2acc7160708..1bdbc5ddceef0 100644 --- a/cli/root.go +++ b/cli/root.go @@ -65,11 +65,17 @@ func Root() *cobra.Command { SilenceUsage: true, Long: `Coder — A tool for provisioning self-hosted development environments. `, - PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { if varSuppressVersion { return nil } + // Login handles checking the versions itself since it + // has a handle to an unauthenticated client. + if cmd.Name() == "login" { + return nil + } + client, err := createClient(cmd) // If the client is unauthenticated we can ignore the check. // The child commands should handle an unauthenticated client. @@ -357,6 +363,10 @@ func FormatCobraError(err error, cmd *cobra.Command) string { } func checkVersions(cmd *cobra.Command, client *codersdk.Client) error { + if varSuppressVersion { + return nil + } + clientVersion := buildinfo.Version() info, err := client.BuildInfo(cmd.Context()) From 5435418020cbfd6114dc80014457dbca1b5ab02b Mon Sep 17 00:00:00 2001 From: sreya Date: Fri, 24 Jun 2022 22:21:55 +0000 Subject: [PATCH 07/17] make gen --- site/src/api/typesGenerated.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index a49dd6b947dcc..d4165c62b0ccd 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -24,7 +24,7 @@ export interface AzureInstanceIdentityToken { readonly encoding: string } -// From codersdk/buildinfo.go:10:6 +// From codersdk/buildinfo.go:11:6 export interface BuildInfoResponse { readonly external_url: string readonly version: string From 9a9bae1955171d1b49cb81ae03f90e718fcef067 Mon Sep 17 00:00:00 2001 From: sreya Date: Fri, 24 Jun 2022 22:42:33 +0000 Subject: [PATCH 08/17] use semver --- buildinfo/buildinfo.go | 19 ++----------------- buildinfo/buildinfo_test.go | 7 ------- 2 files changed, 2 insertions(+), 24 deletions(-) diff --git a/buildinfo/buildinfo.go b/buildinfo/buildinfo.go index dfd0e77c232f8..270f66f3be30e 100644 --- a/buildinfo/buildinfo.go +++ b/buildinfo/buildinfo.go @@ -3,7 +3,6 @@ package buildinfo import ( "fmt" "runtime/debug" - "strings" "sync" "time" @@ -61,25 +60,11 @@ func Version() string { func VersionsMatch(v1, v2 string) bool { // Developer versions are disregarded...hopefully they know what they are // doing. - if strings.HasPrefix(v1, develPrefix) || strings.HasPrefix(v2, develPrefix) { + if semver.Prerelease(v1) != "" || semver.Prerelease(v2) != "" { return true } - v1Toks := strings.Split(v1, ".") - v2Toks := strings.Split(v2, ".") - - // Versions should be formatted as "..". - // We assume malformed versions are evidence of a bug and return false. - if len(v1Toks) < 3 || len(v2Toks) < 3 { - return false - } - - // Slice off the patch suffix. Patch versions should be non-breaking - // changes. - v1MajorMinor := strings.Join(v1Toks[:2], ".") - v2MajorMinor := strings.Join(v2Toks[:2], ".") - - return v1MajorMinor == v2MajorMinor + return semver.Compare(semver.MajorMinor(v1), semver.MajorMinor(v2)) == 0 } // ExternalURL returns a URL referencing the current Coder version. diff --git a/buildinfo/buildinfo_test.go b/buildinfo/buildinfo_test.go index e671476bea583..dcffe7fa671bc 100644 --- a/buildinfo/buildinfo_test.go +++ b/buildinfo/buildinfo_test.go @@ -48,13 +48,6 @@ func TestBuildInfo(t *testing.T) { v2: "v1.2.3", expectMatch: true, }, - // Test that we return false if a version is malformed. - { - name: "MalformedIgnored", - v1: "v1.2.3", - v2: "v1.2", - expectMatch: false, - }, // Test that we return true if a developer version is detected. // Developers do not need to be warned of mismatched versions. { From cc6baed55547304448a995eb8fadeab8632d6e34 Mon Sep 17 00:00:00 2001 From: sreya Date: Fri, 24 Jun 2022 22:54:59 +0000 Subject: [PATCH 09/17] Revert "use semver" This reverts commit 9a9bae1955171d1b49cb81ae03f90e718fcef067. --- buildinfo/buildinfo.go | 19 +++++++++++++++++-- buildinfo/buildinfo_test.go | 7 +++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/buildinfo/buildinfo.go b/buildinfo/buildinfo.go index 270f66f3be30e..dfd0e77c232f8 100644 --- a/buildinfo/buildinfo.go +++ b/buildinfo/buildinfo.go @@ -3,6 +3,7 @@ package buildinfo import ( "fmt" "runtime/debug" + "strings" "sync" "time" @@ -60,11 +61,25 @@ func Version() string { func VersionsMatch(v1, v2 string) bool { // Developer versions are disregarded...hopefully they know what they are // doing. - if semver.Prerelease(v1) != "" || semver.Prerelease(v2) != "" { + if strings.HasPrefix(v1, develPrefix) || strings.HasPrefix(v2, develPrefix) { return true } - return semver.Compare(semver.MajorMinor(v1), semver.MajorMinor(v2)) == 0 + v1Toks := strings.Split(v1, ".") + v2Toks := strings.Split(v2, ".") + + // Versions should be formatted as "..". + // We assume malformed versions are evidence of a bug and return false. + if len(v1Toks) < 3 || len(v2Toks) < 3 { + return false + } + + // Slice off the patch suffix. Patch versions should be non-breaking + // changes. + v1MajorMinor := strings.Join(v1Toks[:2], ".") + v2MajorMinor := strings.Join(v2Toks[:2], ".") + + return v1MajorMinor == v2MajorMinor } // ExternalURL returns a URL referencing the current Coder version. diff --git a/buildinfo/buildinfo_test.go b/buildinfo/buildinfo_test.go index dcffe7fa671bc..e671476bea583 100644 --- a/buildinfo/buildinfo_test.go +++ b/buildinfo/buildinfo_test.go @@ -48,6 +48,13 @@ func TestBuildInfo(t *testing.T) { v2: "v1.2.3", expectMatch: true, }, + // Test that we return false if a version is malformed. + { + name: "MalformedIgnored", + v1: "v1.2.3", + v2: "v1.2", + expectMatch: false, + }, // Test that we return true if a developer version is detected. // Developers do not need to be warned of mismatched versions. { From 6e8c3c0260b7368585f4bc22122b10eab346b84c Mon Sep 17 00:00:00 2001 From: sreya Date: Fri, 24 Jun 2022 23:02:25 +0000 Subject: [PATCH 10/17] use install script link --- cli/root.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cli/root.go b/cli/root.go index 1bdbc5ddceef0..ef0b21dfc8470 100644 --- a/cli/root.go +++ b/cli/root.go @@ -377,12 +377,13 @@ func checkVersions(cmd *cobra.Command, client *codersdk.Client) error { fmtWarningText := `client/server versions do not match client version: %s server version: %s -download the appropriate version from https://github.com/coder/coder/releases/tag/%s +to download the appropriate version run 'curl -L https://coder.com/install.sh | sh -s -- --version %s' ` if !buildinfo.VersionsMatch(clientVersion, info.Version) { warn := cliui.Styles.Warn.Copy().Align(lipgloss.Left) - _, _ = fmt.Fprintf(cmd.OutOrStdout(), warn.Render(fmtWarningText), clientVersion, info.Version, info.TrimmedVersion()) + // Trim the leading 'v', our install.sh script does not handle this case well. + _, _ = fmt.Fprintf(cmd.OutOrStdout(), warn.Render(fmtWarningText), clientVersion, info.Version, info.TrimmedVersion()[1:]) _, _ = fmt.Fprintln(cmd.OutOrStdout()) } From 67966e77b018350d0b2278b3498b267298f5eaa9 Mon Sep 17 00:00:00 2001 From: sreya Date: Mon, 27 Jun 2022 19:16:34 +0000 Subject: [PATCH 11/17] some merge woes --- cli/root.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cli/root.go b/cli/root.go index 47a2b85f00d7c..b7678bb016515 100644 --- a/cli/root.go +++ b/cli/root.go @@ -47,7 +47,7 @@ const ( var ( errUnauthenticated = xerrors.New(notLoggedInMessage) varSuppressVersion = false - envSessionToken = "CODER_SESSION_TOKEN" + envSessionToken = "CODER_SESSION_TOKEN" ) func init() { @@ -128,8 +128,6 @@ func Root() *cobra.Command { cmd.PersistentFlags().String(varURL, "", "Specify the URL to your deployment.") cliflag.BoolVarP(cmd.PersistentFlags(), &varSuppressVersion, "no-version-warning", "", envNoVersionCheck, false, "Suppress warning when client and server versions do not match.") -======= ->>>>>>> main _ = cmd.PersistentFlags().MarkHidden(varAgentToken) cliflag.String(cmd.PersistentFlags(), varAgentURL, "", "CODER_AGENT_URL", "", "Specify the URL for an agent to access your deployment.") _ = cmd.PersistentFlags().MarkHidden(varAgentURL) From d8551726ca86995a387f65ed6c005984ceea8a7f Mon Sep 17 00:00:00 2001 From: sreya Date: Mon, 27 Jun 2022 20:23:48 +0000 Subject: [PATCH 12/17] some stuff --- cli/root.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cli/root.go b/cli/root.go index b7678bb016515..2c96b1a3f57c2 100644 --- a/cli/root.go +++ b/cli/root.go @@ -128,6 +128,8 @@ func Root() *cobra.Command { cmd.PersistentFlags().String(varURL, "", "Specify the URL to your deployment.") cliflag.BoolVarP(cmd.PersistentFlags(), &varSuppressVersion, "no-version-warning", "", envNoVersionCheck, false, "Suppress warning when client and server versions do not match.") + cliflag.String(cmd.PersistentFlags(), varToken, "", envSessionToken, "", fmt.Sprintf("Specify an authentication token. For security reasons setting %s is preferred.", envSessionToken)) + cliflag.String(cmd.PersistentFlags(), varAgentToken, "", "CODER_AGENT_TOKEN", "", "Specify an agent authentication token.") _ = cmd.PersistentFlags().MarkHidden(varAgentToken) cliflag.String(cmd.PersistentFlags(), varAgentURL, "", "CODER_AGENT_URL", "", "Specify the URL for an agent to access your deployment.") _ = cmd.PersistentFlags().MarkHidden(varAgentURL) From 54cb6646b0a99be26d18c99e3ae94ea1ae559e63 Mon Sep 17 00:00:00 2001 From: sreya Date: Tue, 28 Jun 2022 21:34:56 +0000 Subject: [PATCH 13/17] more stuff --- buildinfo/buildinfo.go | 21 ++------------------- buildinfo/buildinfo_test.go | 11 ----------- cli/root.go | 2 +- codersdk/buildinfo.go | 13 +++++-------- 4 files changed, 8 insertions(+), 39 deletions(-) diff --git a/buildinfo/buildinfo.go b/buildinfo/buildinfo.go index dfd0e77c232f8..f9b10c99ac847 100644 --- a/buildinfo/buildinfo.go +++ b/buildinfo/buildinfo.go @@ -3,7 +3,6 @@ package buildinfo import ( "fmt" "runtime/debug" - "strings" "sync" "time" @@ -59,27 +58,11 @@ func Version() string { // disregarded. If it detects that either version is a developer build it // returns true. func VersionsMatch(v1, v2 string) bool { - // Developer versions are disregarded...hopefully they know what they are - // doing. - if strings.HasPrefix(v1, develPrefix) || strings.HasPrefix(v2, develPrefix) { + if semver.Prerelease(v1) == "-devel" || semver.Prerelease(v2) == "-devel" { return true } - v1Toks := strings.Split(v1, ".") - v2Toks := strings.Split(v2, ".") - - // Versions should be formatted as "..". - // We assume malformed versions are evidence of a bug and return false. - if len(v1Toks) < 3 || len(v2Toks) < 3 { - return false - } - - // Slice off the patch suffix. Patch versions should be non-breaking - // changes. - v1MajorMinor := strings.Join(v1Toks[:2], ".") - v2MajorMinor := strings.Join(v2Toks[:2], ".") - - return v1MajorMinor == v2MajorMinor + return semver.MajorMinor(v1) == semver.MajorMinor(v2) } // ExternalURL returns a URL referencing the current Coder version. diff --git a/buildinfo/buildinfo_test.go b/buildinfo/buildinfo_test.go index e671476bea583..70a890156eaf9 100644 --- a/buildinfo/buildinfo_test.go +++ b/buildinfo/buildinfo_test.go @@ -48,13 +48,6 @@ func TestBuildInfo(t *testing.T) { v2: "v1.2.3", expectMatch: true, }, - // Test that we return false if a version is malformed. - { - name: "MalformedIgnored", - v1: "v1.2.3", - v2: "v1.2", - expectMatch: false, - }, // Test that we return true if a developer version is detected. // Developers do not need to be warned of mismatched versions. { @@ -86,10 +79,6 @@ func TestBuildInfo(t *testing.T) { } for _, c := range cases { - // It's very important to do this since we're running the tests - // in parallel. Otherwise you will likely get the last element - // in the list since the goroutines will likely start executing - // after the for loop has completed. c := c t.Run(c.name, func(t *testing.T) { t.Parallel() diff --git a/cli/root.go b/cli/root.go index 2c96b1a3f57c2..738155421b560 100644 --- a/cli/root.go +++ b/cli/root.go @@ -127,7 +127,7 @@ func Root() *cobra.Command { cmd.SetUsageTemplate(usageTemplate()) cmd.PersistentFlags().String(varURL, "", "Specify the URL to your deployment.") - cliflag.BoolVarP(cmd.PersistentFlags(), &varSuppressVersion, "no-version-warning", "", envNoVersionCheck, false, "Suppress warning when client and server versions do not match.") + cliflag.BoolVarP(cmd.Flags(), &varSuppressVersion, "no-version-warning", "", envNoVersionCheck, false, "Suppress warning when client and server versions do not match.") cliflag.String(cmd.PersistentFlags(), varToken, "", envSessionToken, "", fmt.Sprintf("Specify an authentication token. For security reasons setting %s is preferred.", envSessionToken)) cliflag.String(cmd.PersistentFlags(), varAgentToken, "", "CODER_AGENT_TOKEN", "", "Specify an agent authentication token.") _ = cmd.PersistentFlags().MarkHidden(varAgentToken) diff --git a/codersdk/buildinfo.go b/codersdk/buildinfo.go index b152103e20e0f..ed2bc43644cf7 100644 --- a/codersdk/buildinfo.go +++ b/codersdk/buildinfo.go @@ -1,10 +1,12 @@ package codersdk import ( - "bytes" "context" "encoding/json" "net/http" + "strings" + + "golang.org/x/mod/semver" ) // BuildInfoResponse contains build information for this instance of Coder. @@ -20,13 +22,8 @@ type BuildInfoResponse struct { // TrimmedVersion trims build information from the version. // E.g. 'v0.7.4-devel+11573034' -> 'v0.7.4'. func (b BuildInfoResponse) TrimmedVersion() string { - // Linter doesn't like strings.Index... - idx := bytes.Index([]byte(b.Version), []byte("-devel")) - if idx < 0 { - return b.Version - } - - return b.Version[:idx] + trimmed := strings.ReplaceAll(b.Version, "-devel", "+devel") + return semver.Canonical(trimmed) } // BuildInfo returns build information for this instance of Coder. From 51301d92c29cf2be3d3a93da6dc8c1e5cc17f7c2 Mon Sep 17 00:00:00 2001 From: sreya Date: Tue, 28 Jun 2022 21:51:40 +0000 Subject: [PATCH 14/17] fix versions --- buildinfo/buildinfo.go | 5 ++++- cli/root.go | 2 +- codersdk/buildinfo.go | 8 +++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/buildinfo/buildinfo.go b/buildinfo/buildinfo.go index f9b10c99ac847..65026d4681f77 100644 --- a/buildinfo/buildinfo.go +++ b/buildinfo/buildinfo.go @@ -3,6 +3,7 @@ package buildinfo import ( "fmt" "runtime/debug" + "strings" "sync" "time" @@ -58,7 +59,9 @@ func Version() string { // disregarded. If it detects that either version is a developer build it // returns true. func VersionsMatch(v1, v2 string) bool { - if semver.Prerelease(v1) == "-devel" || semver.Prerelease(v2) == "-devel" { + // Developer versions are disregarded...hopefully they know what they are + // doing. + if strings.HasPrefix(v1, develPrefix) || strings.HasPrefix(v2, develPrefix) { return true } diff --git a/cli/root.go b/cli/root.go index 738155421b560..b18751012a7eb 100644 --- a/cli/root.go +++ b/cli/root.go @@ -384,7 +384,7 @@ to download the appropriate version run 'curl -L https://coder.com/install.sh | if !buildinfo.VersionsMatch(clientVersion, info.Version) { warn := cliui.Styles.Warn.Copy().Align(lipgloss.Left) // Trim the leading 'v', our install.sh script does not handle this case well. - _, _ = fmt.Fprintf(cmd.OutOrStdout(), warn.Render(fmtWarningText), clientVersion, info.Version, info.TrimmedVersion()[1:]) + _, _ = fmt.Fprintf(cmd.OutOrStdout(), warn.Render(fmtWarningText), clientVersion, info.Version, strings.TrimPrefix(info.CanonicalVersion(), "v")) _, _ = fmt.Fprintln(cmd.OutOrStdout()) } diff --git a/codersdk/buildinfo.go b/codersdk/buildinfo.go index ed2bc43644cf7..a8ba01e3716cb 100644 --- a/codersdk/buildinfo.go +++ b/codersdk/buildinfo.go @@ -19,10 +19,12 @@ type BuildInfoResponse struct { Version string `json:"version"` } -// TrimmedVersion trims build information from the version. +// CanonicalVersion trims build information from the version. // E.g. 'v0.7.4-devel+11573034' -> 'v0.7.4'. -func (b BuildInfoResponse) TrimmedVersion() string { - trimmed := strings.ReplaceAll(b.Version, "-devel", "+devel") +func (b BuildInfoResponse) CanonicalVersion() string { + // We do a little hack here to massage the string into a form + // that works well with semver. + trimmed := strings.ReplaceAll(b.Version, "-devel+", "+devel-") return semver.Canonical(trimmed) } From 636d5dbc694acb86c888de3d2d02d46cb9d86935 Mon Sep 17 00:00:00 2001 From: sreya Date: Tue, 28 Jun 2022 22:13:34 +0000 Subject: [PATCH 15/17] remove race condition --- buildinfo/buildinfo_test.go | 8 ++++++++ cli/root.go | 12 ++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/buildinfo/buildinfo_test.go b/buildinfo/buildinfo_test.go index 70a890156eaf9..12cc8c99a3ee7 100644 --- a/buildinfo/buildinfo_test.go +++ b/buildinfo/buildinfo_test.go @@ -56,6 +56,14 @@ func TestBuildInfo(t *testing.T) { v2: "v1.2.3", expectMatch: true, }, + // Our CI instance uses a "-devel" prerelease + // flag. This is not the same as a developer WIP build. + { + name: "DevelPreleaseNotIgnored", + v1: "v1.1.1-devel+123abac", + v2: "v1.2.3", + expectMatch: false, + }, { name: "MajorMismatch", v1: "v1.2.3", diff --git a/cli/root.go b/cli/root.go index b18751012a7eb..19110e58a056e 100644 --- a/cli/root.go +++ b/cli/root.go @@ -4,6 +4,7 @@ import ( "fmt" "net/url" "os" + "strconv" "strings" "time" @@ -41,12 +42,12 @@ const ( varForceTty = "force-tty" notLoggedInMessage = "You are not logged in. Try logging in using 'coder login '." - envNoVersionCheck = "CODER_NO_VERSION_WARNING" + noVersionCheckFlag = "no-version-warning" + envNoVersionCheck = "CODER_NO_VERSION_WARNING" ) var ( errUnauthenticated = xerrors.New(notLoggedInMessage) - varSuppressVersion = false envSessionToken = "CODER_SESSION_TOKEN" ) @@ -60,6 +61,8 @@ func init() { } func Root() *cobra.Command { + var varSuppressVersion bool + cmd := &cobra.Command{ Use: "coder", SilenceErrors: true, @@ -127,7 +130,7 @@ func Root() *cobra.Command { cmd.SetUsageTemplate(usageTemplate()) cmd.PersistentFlags().String(varURL, "", "Specify the URL to your deployment.") - cliflag.BoolVarP(cmd.Flags(), &varSuppressVersion, "no-version-warning", "", envNoVersionCheck, false, "Suppress warning when client and server versions do not match.") + cliflag.BoolVarP(cmd.PersistentFlags(), &varSuppressVersion, noVersionCheckFlag, "", envNoVersionCheck, false, "Suppress warning when client and server versions do not match.") cliflag.String(cmd.PersistentFlags(), varToken, "", envSessionToken, "", fmt.Sprintf("Specify an authentication token. For security reasons setting %s is preferred.", envSessionToken)) cliflag.String(cmd.PersistentFlags(), varAgentToken, "", "CODER_AGENT_TOKEN", "", "Specify an agent authentication token.") _ = cmd.PersistentFlags().MarkHidden(varAgentToken) @@ -364,7 +367,8 @@ func FormatCobraError(err error, cmd *cobra.Command) string { } func checkVersions(cmd *cobra.Command, client *codersdk.Client) error { - if varSuppressVersion { + flag := cmd.Flag("no-version-warning") + if suppress, _ := strconv.ParseBool(flag.Value.String()); suppress { return nil } From d36e134f35528818ba3b5431b0463248da8cf22b Mon Sep 17 00:00:00 2001 From: sreya Date: Tue, 28 Jun 2022 22:50:48 +0000 Subject: [PATCH 16/17] make gen --- site/src/api/typesGenerated.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 06dcc0083ba1c..57f7cdba81800 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -36,7 +36,7 @@ export interface AzureInstanceIdentityToken { readonly encoding: string } -// From codersdk/buildinfo.go:11:6 +// From codersdk/buildinfo.go:13:6 export interface BuildInfoResponse { readonly external_url: string readonly version: string From b07de2424ef7355b60ba5d23b20dd24be63d78a0 Mon Sep 17 00:00:00 2001 From: sreya Date: Wed, 29 Jun 2022 00:59:26 +0000 Subject: [PATCH 17/17] pr comments --- cli/root.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cli/root.go b/cli/root.go index 19110e58a056e..9dc8baf8914ba 100644 --- a/cli/root.go +++ b/cli/root.go @@ -379,10 +379,8 @@ func checkVersions(cmd *cobra.Command, client *codersdk.Client) error { return xerrors.Errorf("build info: %w", err) } - fmtWarningText := `client/server versions do not match -client version: %s -server version: %s -to download the appropriate version run 'curl -L https://coder.com/install.sh | sh -s -- --version %s' + fmtWarningText := `version mismatch: client %s, server %s +download the server version with: 'curl -L https://coder.com/install.sh | sh -s -- --version %s' ` if !buildinfo.VersionsMatch(clientVersion, info.Version) {