mirror of https://github.com/coder/coder.git
feat: add customizable upgrade message on client/server version mismatch (#11587)
This commit is contained in:
parent
adbb025e74
commit
0c30dde9b5
|
@ -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.
|
||||
|
|
45
cli/root.go
45
cli/root.go
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 | | |
|
||||
|
|
|
@ -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
|
||||
|
||||
| | |
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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[] = [
|
||||
|
|
Loading…
Reference in New Issue