feat: add customizable upgrade message on client/server version mismatch (#11587)

This commit is contained in:
Jon Ayers
2024-01-30 17:11:37 -06:00
committed by GitHub
parent adbb025e74
commit 0c30dde9b5
16 changed files with 191 additions and 20 deletions
+2 -1
View File
@@ -18,6 +18,7 @@ import (
"github.com/coder/pretty"
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/userpassword"
@@ -175,7 +176,7 @@ func (r *RootCmd) login() *clibase.Cmd {
// 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 = r.checkVersions(inv, client)
err = r.checkVersions(inv, client, buildinfo.Version())
if err != nil {
// Checking versions isn't a fatal error so we print a warning
// and proceed.
+28 -17
View File
@@ -602,7 +602,7 @@ func (r *RootCmd) PrintWarnings(client *codersdk.Client) clibase.MiddlewareFunc
warningErr = make(chan error)
)
go func() {
versionErr <- r.checkVersions(inv, client)
versionErr <- r.checkVersions(inv, client, buildinfo.Version())
close(versionErr)
}()
@@ -812,7 +812,12 @@ func formatExamples(examples ...example) string {
return sb.String()
}
func (r *RootCmd) checkVersions(i *clibase.Invocation, client *codersdk.Client) error {
// checkVersions checks to see if there's a version mismatch between the client
// and server and prints a message nudging the user to upgrade if a mismatch
// is detected. forceCheck is a test flag and should always be false in production.
//
//nolint:revive
func (r *RootCmd) checkVersions(i *clibase.Invocation, client *codersdk.Client, clientVersion string) error {
if r.noVersionCheck {
return nil
}
@@ -820,30 +825,26 @@ func (r *RootCmd) checkVersions(i *clibase.Invocation, client *codersdk.Client)
ctx, cancel := context.WithTimeout(i.Context(), 10*time.Second)
defer cancel()
clientVersion := buildinfo.Version()
info, err := client.BuildInfo(ctx)
serverInfo, err := client.BuildInfo(ctx)
// Avoid printing errors that are connection-related.
if isConnectionError(err) {
return nil
}
if err != nil {
return xerrors.Errorf("build info: %w", err)
}
fmtWarningText := `version mismatch: client %s, server %s
`
// Our installation script doesn't work on Windows, so instead we direct the user
// to the GitHub release page to download the latest installer.
if runtime.GOOS == "windows" {
fmtWarningText += `download the server version from: https://github.com/coder/coder/releases/v%s`
} else {
fmtWarningText += `download the server version with: 'curl -L https://coder.com/install.sh | sh -s -- --version %s'`
}
if !buildinfo.VersionsMatch(clientVersion, serverInfo.Version) {
upgradeMessage := defaultUpgradeMessage(serverInfo.CanonicalVersion())
if serverInfo.UpgradeMessage != "" {
upgradeMessage = serverInfo.UpgradeMessage
}
if !buildinfo.VersionsMatch(clientVersion, info.Version) {
warn := cliui.DefaultStyles.Warn
_, _ = fmt.Fprintf(i.Stderr, pretty.Sprint(warn, fmtWarningText), clientVersion, info.Version, strings.TrimPrefix(info.CanonicalVersion(), "v"))
fmtWarningText := "version mismatch: client %s, server %s\n%s"
fmtWarn := pretty.Sprint(cliui.DefaultStyles.Warn, fmtWarningText)
warning := fmt.Sprintf(fmtWarn, clientVersion, serverInfo.Version, upgradeMessage)
_, _ = fmt.Fprint(i.Stderr, warning)
_, _ = fmt.Fprintln(i.Stderr)
}
@@ -1216,3 +1217,13 @@ func SlimUnsupported(w io.Writer, cmd string) {
//nolint:revive
os.Exit(1)
}
func defaultUpgradeMessage(version string) string {
// Our installation script doesn't work on Windows, so instead we direct the user
// to the GitHub release page to download the latest installer.
version = strings.TrimPrefix(version, "v")
if runtime.GOOS == "windows" {
return fmt.Sprintf("download the server version from: https://github.com/coder/coder/releases/v%s", version)
}
return fmt.Sprintf("download the server version with: 'curl -L https://coder.com/install.sh | sh -s -- --version %s'", version)
}
+94
View File
@@ -1,12 +1,24 @@
package cli
import (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"os"
"runtime"
"testing"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/pretty"
)
func Test_formatExamples(t *testing.T) {
@@ -84,3 +96,85 @@ func TestMain(m *testing.M) {
goleak.IgnoreTopFunction("github.com/lib/pq.NewDialListener"),
)
}
func Test_checkVersions(t *testing.T) {
t.Parallel()
t.Run("CustomUpgradeMessage", func(t *testing.T) {
t.Parallel()
expectedUpgradeMessage := "My custom upgrade message"
srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.BuildInfoResponse{
ExternalURL: buildinfo.ExternalURL(),
// Provide a version that will not match
Version: "v1.0.0",
AgentAPIVersion: coderd.AgentAPIVersionREST,
// does not matter what the url is
DashboardURL: "https://example.com",
WorkspaceProxy: false,
UpgradeMessage: expectedUpgradeMessage,
})
}))
defer srv.Close()
surl, err := url.Parse(srv.URL)
require.NoError(t, err)
client := codersdk.New(surl)
r := &RootCmd{}
cmd, err := r.Command(nil)
require.NoError(t, err)
var buf bytes.Buffer
inv := cmd.Invoke()
inv.Stderr = &buf
err = r.checkVersions(inv, client, "v2.0.0")
require.NoError(t, err)
fmtOutput := fmt.Sprintf("version mismatch: client v2.0.0, server v1.0.0\n%s", expectedUpgradeMessage)
expectedOutput := fmt.Sprintln(pretty.Sprint(cliui.DefaultStyles.Warn, fmtOutput))
require.Equal(t, expectedOutput, buf.String())
})
t.Run("DefaultUpgradeMessage", func(t *testing.T) {
t.Parallel()
srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.BuildInfoResponse{
ExternalURL: buildinfo.ExternalURL(),
// Provide a version that will not match
Version: "v1.0.0",
AgentAPIVersion: coderd.AgentAPIVersionREST,
// does not matter what the url is
DashboardURL: "https://example.com",
WorkspaceProxy: false,
UpgradeMessage: "",
})
}))
defer srv.Close()
surl, err := url.Parse(srv.URL)
require.NoError(t, err)
client := codersdk.New(surl)
r := &RootCmd{}
cmd, err := r.Command(nil)
require.NoError(t, err)
var buf bytes.Buffer
inv := cmd.Invoke()
inv.Stderr = &buf
err = r.checkVersions(inv, client, "v2.0.0")
require.NoError(t, err)
fmtOutput := fmt.Sprintf("version mismatch: client v2.0.0, server v1.0.0\n%s", defaultUpgradeMessage("v1.0.0"))
expectedOutput := fmt.Sprintln(pretty.Sprint(cliui.DefaultStyles.Warn, fmtOutput))
require.Equal(t, expectedOutput, buf.String())
})
}
+5
View File
@@ -65,6 +65,11 @@ CLIENT OPTIONS:
These options change the behavior of how clients interact with the Coder.
Clients include the coder cli, vs code extension, and the web UI.
--cli-upgrade-message string, $CODER_CLI_UPGRADE_MESSAGE
The upgrade message to display to users when a client/server mismatch
is detected. By default it instructs users to update using 'curl -L
https://coder.com/install.sh | sh'.
--ssh-config-options string-array, $CODER_SSH_CONFIG_OPTIONS
These SSH config options will override the default SSH config options.
Provide options in "key=value" or "key value" format separated by
+5
View File
@@ -433,6 +433,11 @@ client:
# incorrectly can break SSH to your deployment, use cautiously.
# (default: <unset>, type: string-array)
sshConfigOptions: []
# The upgrade message to display to users when a client/server mismatch is
# detected. By default it instructs users to update using 'curl -L
# https://coder.com/install.sh | sh'.
# (default: <unset>, type: string)
cliUpgradeMessage: ""
# The renderer to use when opening a web terminal. Valid values are 'canvas',
# 'webgl', or 'dom'.
# (default: canvas, type: string)