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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 191 additions and 20 deletions

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.

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)
}

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())
})
}

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

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)

7
coderd/apidoc/docs.go generated
View File

@ -8329,6 +8329,10 @@ const docTemplate = `{
"description": "ExternalURL references the current Coder version.\nFor production builds, this will link directly to a release. For development builds, this will link to a commit.",
"type": "string"
},
"upgrade_message": {
"description": "UpgradeMessage is the message displayed to users when an outdated client\nis detected.",
"type": "string"
},
"version": {
"description": "Version returns the semantic version of the build.",
"type": "string"
@ -9043,6 +9047,9 @@ const docTemplate = `{
"cache_directory": {
"type": "string"
},
"cli_upgrade_message": {
"type": "string"
},
"config": {
"type": "string"
},

View File

@ -7418,6 +7418,10 @@
"description": "ExternalURL references the current Coder version.\nFor production builds, this will link directly to a release. For development builds, this will link to a commit.",
"type": "string"
},
"upgrade_message": {
"description": "UpgradeMessage is the message displayed to users when an outdated client\nis detected.",
"type": "string"
},
"version": {
"description": "Version returns the semantic version of the build.",
"type": "string"
@ -8079,6 +8083,9 @@
"cache_directory": {
"type": "string"
},
"cli_upgrade_message": {
"type": "string"
},
"config": {
"type": "string"
},

View File

@ -645,7 +645,7 @@ func New(options *Options) *API {
// All CSP errors will be logged
r.Post("/csp/reports", api.logReportCSPViolations)
r.Get("/buildinfo", buildInfo(api.AccessURL))
r.Get("/buildinfo", buildInfo(api.AccessURL, api.DeploymentValues.CLIUpgradeMessage.String()))
// /regions is overridden in the enterprise version
r.Group(func(r chi.Router) {
r.Use(apiKeyMiddleware)

View File

@ -68,7 +68,7 @@ func (api *API) deploymentStats(rw http.ResponseWriter, r *http.Request) {
// @Tags General
// @Success 200 {object} codersdk.BuildInfoResponse
// @Router /buildinfo [get]
func buildInfo(accessURL *url.URL) http.HandlerFunc {
func buildInfo(accessURL *url.URL, upgradeMessage string) http.HandlerFunc {
return func(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.BuildInfoResponse{
ExternalURL: buildinfo.ExternalURL(),
@ -76,6 +76,7 @@ func buildInfo(accessURL *url.URL) http.HandlerFunc {
AgentAPIVersion: AgentAPIVersionREST,
DashboardURL: accessURL.String(),
WorkspaceProxy: false,
UpgradeMessage: upgradeMessage,
})
}
}

View File

@ -188,6 +188,7 @@ type DeploymentValues struct {
WebTerminalRenderer clibase.String `json:"web_terminal_renderer,omitempty" typescript:",notnull"`
AllowWorkspaceRenames clibase.Bool `json:"allow_workspace_renames,omitempty" typescript:",notnull"`
Healthcheck HealthcheckConfig `json:"healthcheck,omitempty" typescript:",notnull"`
CLIUpgradeMessage clibase.String `json:"cli_upgrade_message,omitempty" typescript:",notnull"`
Config clibase.YAMLConfigPath `json:"config,omitempty" typescript:",notnull"`
WriteConfig clibase.Bool `json:"write_config,omitempty" typescript:",notnull"`
@ -1780,6 +1781,16 @@ when required by your organization's security policy.`,
Value: &c.SSHConfig.SSHConfigOptions,
Hidden: false,
},
{
Name: "CLI Upgrade Message",
Description: "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'.",
Flag: "cli-upgrade-message",
Env: "CODER_CLI_UPGRADE_MESSAGE",
YAML: "cliUpgradeMessage",
Group: &deploymentGroupClient,
Value: &c.CLIUpgradeMessage,
Hidden: false,
},
{
Name: "Write Config",
Description: `
@ -2052,6 +2063,10 @@ type BuildInfoResponse struct {
// AgentAPIVersion is the current version of the Agent API (back versions
// MAY still be supported).
AgentAPIVersion string `json:"agent_api_version"`
// UpgradeMessage is the message displayed to users when an outdated client
// is detected.
UpgradeMessage string `json:"upgrade_message"`
}
type WorkspaceProxyBuildInfo struct {

2
docs/api/general.md generated
View File

@ -56,6 +56,7 @@ curl -X GET http://coder-server:8080/api/v2/buildinfo \
"agent_api_version": "string",
"dashboard_url": "string",
"external_url": "string",
"upgrade_message": "string",
"version": "string",
"workspace_proxy": true
}
@ -157,6 +158,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \
"autobuild_poll_interval": 0,
"browser_only": true,
"cache_directory": "string",
"cli_upgrade_message": "string",
"config": "string",
"config_ssh": {
"deploymentName": "string",

5
docs/api/schemas.md generated
View File

@ -1445,6 +1445,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"agent_api_version": "string",
"dashboard_url": "string",
"external_url": "string",
"upgrade_message": "string",
"version": "string",
"workspace_proxy": true
}
@ -1457,6 +1458,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| `agent_api_version` | string | false | | Agent api version is the current version of the Agent API (back versions MAY still be supported). |
| `dashboard_url` | string | false | | Dashboard URL is the URL to hit the deployment's dashboard. For external workspace proxies, this is the coderd they are connected to. |
| `external_url` | string | false | | External URL references the current Coder version. For production builds, this will link directly to a release. For development builds, this will link to a commit. |
| `upgrade_message` | string | false | | Upgrade message is the message displayed to users when an outdated client is detected. |
| `version` | string | false | | Version returns the semantic version of the build. |
| `workspace_proxy` | boolean | false | | |
@ -2131,6 +2133,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"autobuild_poll_interval": 0,
"browser_only": true,
"cache_directory": "string",
"cli_upgrade_message": "string",
"config": "string",
"config_ssh": {
"deploymentName": "string",
@ -2497,6 +2500,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"autobuild_poll_interval": 0,
"browser_only": true,
"cache_directory": "string",
"cli_upgrade_message": "string",
"config": "string",
"config_ssh": {
"deploymentName": "string",
@ -2758,6 +2762,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| `autobuild_poll_interval` | integer | false | | |
| `browser_only` | boolean | false | | |
| `cache_directory` | string | false | | |
| `cli_upgrade_message` | string | false | | |
| `config` | string | false | | |
| `config_ssh` | [codersdk.SSHConfig](#codersdksshconfig) | false | | |
| `dangerous` | [codersdk.DangerousConfig](#codersdkdangerousconfig) | false | | |

10
docs/cli/server.md generated
View File

@ -73,6 +73,16 @@ Block peer-to-peer (aka. direct) workspace connections. All workspace connection
Whether Coder only allows connections to workspaces via the browser.
### --cli-upgrade-message
| | |
| ----------- | --------------------------------------- |
| Type | <code>string</code> |
| Environment | <code>$CODER_CLI_UPGRADE_MESSAGE</code> |
| YAML | <code>client.cliUpgradeMessage</code> |
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'.
### --cache-dir
| | |

View File

@ -66,6 +66,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

View File

@ -163,6 +163,7 @@ export interface BuildInfoResponse {
readonly dashboard_url: string;
readonly workspace_proxy: boolean;
readonly agent_api_version: string;
readonly upgrade_message: string;
}
// From codersdk/insights.go
@ -438,6 +439,7 @@ export interface DeploymentValues {
readonly web_terminal_renderer?: string;
readonly allow_workspace_renames?: boolean;
readonly healthcheck?: HealthcheckConfig;
readonly cli_upgrade_message?: string;
readonly config?: string;
readonly write_config?: boolean;
readonly address?: string;

View File

@ -199,6 +199,7 @@ export const MockBuildInfo: TypesGen.BuildInfoResponse = {
version: "v99.999.9999+c9cdf14",
dashboard_url: "https:///mock-url",
workspace_proxy: false,
upgrade_message: "My custom upgrade message",
};
export const MockSupportLinks: TypesGen.LinkConfig[] = [