Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,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, false)
if err != nil {
// Checking versions isn't a fatal error so we print a warning
// and proceed.
Expand Down
28 changes: 13 additions & 15 deletions cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, false)
close(versionErr)
}()

Expand Down Expand Up @@ -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, forceCheck bool) error {
if r.noVersionCheck {
return nil
}
Expand All @@ -826,24 +831,17 @@ func (r *RootCmd) checkVersions(i *clibase.Invocation, client *codersdk.Client)
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'`
}
fmtWarningText := "version mismatch: client %s, server %s\n%s"

fmtWarn := pretty.Sprint(cliui.DefaultStyles.Warn, fmtWarningText)
warning := fmt.Sprintf(fmtWarn, info.Version, strings.TrimPrefix(info.CanonicalVersion(), "v"), info.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"))
if !buildinfo.VersionsMatch(clientVersion, info.Version) || forceCheck {
_, _ = fmt.Fprint(i.Stderr, warning)
_, _ = fmt.Fprintln(i.Stderr)
}

Expand Down
45 changes: 45 additions & 0 deletions cli/root_internal_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
package cli

import (
"bytes"
"fmt"
"os"
"runtime"
"testing"

"github.com/stretchr/testify/require"
"go.uber.org/goleak"

"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/pretty"
)

func Test_formatExamples(t *testing.T) {
Expand Down Expand Up @@ -84,3 +91,41 @@ func TestMain(m *testing.M) {
goleak.IgnoreTopFunction("github.com/lib/pq.NewDialListener"),
)
}

func Test_checkVersions(t *testing.T) {
t.Parallel()

t.Run("CustomInstallMessage", func(t *testing.T) {
t.Parallel()

var (
expectedUpgradeMessage = "My custom upgrade message"
dv = coderdtest.DeploymentValues(t)
)
dv.CLIUpgradeMessage = clibase.String(expectedUpgradeMessage)

ownerClient := coderdtest.New(t, &coderdtest.Options{
DeploymentValues: dv,
})
owner := coderdtest.CreateFirstUser(t, ownerClient)

// Create an unprivileged user to ensure the message can be printed
// to any Coder user.
memberClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)

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, memberClient, true)
require.NoError(t, err)

expectedOutput := fmt.Sprintln(pretty.Sprint(cliui.DefaultStyles.Warn, expectedUpgradeMessage))
require.Equal(t, expectedOutput, buf.String())
})
}
5 changes: 5 additions & 0 deletions cli/testdata/coder_server_--help.golden
Original file line number Diff line number Diff line change
Expand Up @@ -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 (default: download the server version with: 'curl -L https://coder.com/install.sh | sh -s -- --version v0.0.0-devel')
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
Expand Down
6 changes: 6 additions & 0 deletions cli/testdata/server-config.yaml.golden
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,12 @@ 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: download the server version with: 'curl -L
# https://coder.com/install.sh | sh -s -- --version v0.0.0-devel', type: string)
cliUpgradeMessage: 'download the server version with: ''curl -L https://coder.com/install.sh | sh -s -- --version v0.0.0-devel'''
# The renderer to use when opening a web terminal. Valid values are 'canvas',
# 'webgl', or 'dom'.
# (default: canvas, type: string)
Expand Down
3 changes: 2 additions & 1 deletion coderd/agentapi/servicebanner_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import (

"golang.org/x/xerrors"

"github.com/stretchr/testify/require"

agentproto "github.com/coder/coder/v2/agent/proto"
"github.com/coder/coder/v2/coderd/appearance"
"github.com/coder/coder/v2/codersdk"
"github.com/stretchr/testify/require"
)

func TestGetServiceBanner(t *testing.T) {
Expand Down
7 changes: 7 additions & 0 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -647,7 +647,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)
Expand Down
3 changes: 2 additions & 1 deletion coderd/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,15 @@ 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(),
Version: buildinfo.Version(),
AgentAPIVersion: AgentAPIVersionREST,
DashboardURL: accessURL.String(),
WorkspaceProxy: false,
UpgradeMessage: upgradeMessage,
})
}
}
Expand Down
28 changes: 28 additions & 0 deletions codersdk/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import (
"context"
"encoding/json"
"flag"
"fmt"
"net/http"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -188,6 +190,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"`
Expand Down Expand Up @@ -1780,6 +1783,17 @@ 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,
Default: defaultUpgradeMessage(),
Hidden: false,
},
{
Name: "Write Config",
Description: `
Expand Down Expand Up @@ -2052,6 +2066,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 {
Expand Down Expand Up @@ -2287,3 +2305,13 @@ func (c *Client) SSHConfiguration(ctx context.Context) (SSHConfigResponse, error
var sshConfig SSHConfigResponse
return sshConfig, json.NewDecoder(res.Body).Decode(&sshConfig)
}

func defaultUpgradeMessage() 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 := buildinfo.Version()
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)
}
2 changes: 2 additions & 0 deletions docs/api/general.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions docs/api/schemas.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions docs/cli/server.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions enterprise/cli/testdata/coder_server_--help.golden
Original file line number Diff line number Diff line change
Expand Up @@ -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 (default: download the server version with: 'curl -L https://coder.com/install.sh | sh -s -- --version v0.0.0-devel')
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
Expand Down
Loading