Skip to content

Commit

Permalink
[gitpod-cli] Add auto-updating capabilities (#19056)
Browse files Browse the repository at this point in the history
* Add version command

* Restructure config package

* Bring back config get and config set

* Support login host without protocol scheme

* Add autoupdate functionality

* Generate update manifest during build

* Better update failure behavior

* Add latest to version command

* Add version update command

* Use cannonical semver form
  • Loading branch information
csweichel authored Nov 13, 2023
1 parent 0e00e3d commit e4bc514
Show file tree
Hide file tree
Showing 23 changed files with 1,070 additions and 109 deletions.
16 changes: 7 additions & 9 deletions components/local-app/BUILD.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const generatePackage = function (goos, goarch, binaryName, mainFile) {
let pkg = {
name,
type: "go",
srcs: ["go.mod", "go.sum", "**/*.go"],
srcs: ["go.mod", "go.sum", "**/*.go", "version.txt"],
deps: [
"components/supervisor-api/go:lib",
"components/gitpod-protocol/go:lib",
Expand All @@ -19,14 +19,12 @@ const generatePackage = function (goos, goarch, binaryName, mainFile) {
packaging: "app",
dontTest: dontTest,
buildCommand: [
"go",
"build",
"-trimpath",
"-ldflags",
"-buildid= -w -s -X 'github.com/gitpod-io/local-app/pkg/constants.Version=commit-${__git_commit}'",
"-o",
binaryName,
mainFile,
"sh",
"-c",
'go build -trimpath -ldflags "-X github.com/gitpod-io/local-app/pkg/constants.GitCommit=${__git_commit} -X github.com/gitpod-io/local-app/pkg/constants.BuildTime=$(date +%s)" -o ' +
binaryName +
" " +
mainFile,
],
},
binaryName,
Expand Down
29 changes: 27 additions & 2 deletions components/local-app/BUILD.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ packages:
- name: docker
type: docker
deps:
- :app
- :app-with-manifest
argdeps:
- imageRepoBase
config:
Expand All @@ -13,7 +13,32 @@ packages:
image:
- ${imageRepoBase}/local-app:${version}
- ${imageRepoBase}/local-app:commit-${__git_commit}

- name: update-manifest
type: go
srcs:
- go.mod
- go.sum
- "**/*.go"
- version.txt
deps:
- components/supervisor-api/go:lib
- components/gitpod-protocol/go:lib
- components/local-app-api/go:lib
- components/public-api/go:lib
config:
packaging: app
dontTest: true
buildCommand: ["go", "build", "-o", "update-manifest", "./main/update-manifest/main.go"]
- name: app-with-manifest
type: generic
deps:
- :app
- :update-manifest
config:
commands:
- ["sh", "-c", "mkdir -p bin && mv components-local-app--app/bin/* bin/"]
- ["sh", "-c", "components-local-app--update-manifest/update-manifest --cwd bin | tee bin/manifest.json"]
- ["rm", "-rf", "components-local-app--update-manifest", "components-local-app--app"]
scripts:
- name: install-cli
description: "Install gitpod-cli as `gitpod` command and add auto-completion. Usage: '. $(leeway run components/local-app:install-cli)'"
Expand Down
5 changes: 5 additions & 0 deletions components/local-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ leeway run components/local-app:install-cli
leeway run components/local-app:cli-completion
```

### Versioning and Release Management

The CLI is versioned independently of other Gitpod artifacts due to its auto-updating behaviour.
To create a new version that existing clients will consume increment the number in `version.txt`. Make sure to use semantic versioning. The minor version can be greater than 10, e.g. `0.342` is a valid version.

## local-app

**Beware**: this is very much work in progress and will likely break things.
Expand Down
37 changes: 37 additions & 0 deletions components/local-app/cmd/config-get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) 2023 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License.AGPL.txt in the project root for license information.

package cmd

import (
"github.com/gitpod-io/local-app/pkg/config"
"github.com/gitpod-io/local-app/pkg/prettyprint"
"github.com/spf13/cobra"
)

var configGetCmd = &cobra.Command{
Use: "get",
Short: "Get an individual config value in the config file",
RunE: func(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true

cfg := config.FromContext(cmd.Context())

return WriteTabular([]struct {
Telemetry bool `header:"Telemetry"`
Autoupdate bool `header:"Autoupdate"`
}{
{Telemetry: cfg.Telemetry.Enabled, Autoupdate: cfg.Autoupdate},
}, configGetOpts.Format, prettyprint.WriterFormatNarrow)
},
}

var configGetOpts struct {
Format formatOpts
}

func init() {
configCmd.AddCommand(configGetCmd)
addFormatFlags(configGetCmd, &configGetOpts.Format)
}
64 changes: 64 additions & 0 deletions components/local-app/cmd/config-set.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright (c) 2023 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License.AGPL.txt in the project root for license information.

package cmd

import (
"log/slog"

"github.com/gitpod-io/local-app/pkg/config"
"github.com/spf13/cobra"
)

var configSetCmd = &cobra.Command{
Use: "set",
Short: "Set an individual config value in the config file",
Long: `Set an individual config value in the config file.
Example:
# Disable telemetry
local-app config set --telemetry=false
# Disable autoupdate
local-app config set --autoupdate=false
# Enable telemetry and autoupdate
local-app config set --telemetry=true --autoupdate=true
`,
RunE: func(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true

var update bool
cfg := config.FromContext(cmd.Context())
if cmd.Flags().Changed("autoupdate") {
cfg.Autoupdate = configSetOpts.Autoupdate
update = true
}
if cmd.Flags().Changed("telemetry") {
cfg.Telemetry.Enabled = configSetOpts.Telemetry
update = true
}
if !update {
return cmd.Help()
}

slog.Debug("updating config")
err := config.SaveConfig(cfg.Filename, cfg)
if err != nil {
return err
}
return nil
},
}

var configSetOpts struct {
Autoupdate bool
Telemetry bool
}

func init() {
configCmd.AddCommand(configSetCmd)
configSetCmd.Flags().BoolVar(&configSetOpts.Autoupdate, "autoupdate", true, "enable/disable autoupdate")
configSetCmd.Flags().BoolVar(&configSetOpts.Telemetry, "telemetry", true, "enable/disable telemetry")
}
4 changes: 4 additions & 0 deletions components/local-app/cmd/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"log/slog"
"net/url"
"os"
"strings"
"time"

"github.com/bufbuild/connect-go"
Expand All @@ -37,6 +38,9 @@ var loginCmd = &cobra.Command{
RunE: func(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true

if !strings.HasPrefix(loginOpts.Host, "http") {
loginOpts.Host = "https://" + loginOpts.Host
}
host, err := url.Parse(loginOpts.Host)
if err != nil {
return fmt.Errorf("cannot parse host %s: %w", loginOpts.Host, err)
Expand Down
11 changes: 10 additions & 1 deletion components/local-app/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/gitpod-io/local-app/pkg/config"
"github.com/gitpod-io/local-app/pkg/constants"
"github.com/gitpod-io/local-app/pkg/prettyprint"
"github.com/gitpod-io/local-app/pkg/selfupdate"
"github.com/gitpod-io/local-app/pkg/telemetry"
"github.com/gookit/color"
"github.com/lmittmann/tint"
Expand Down Expand Up @@ -97,9 +98,17 @@ var rootCmd = &cobra.Command{
if gpctx, err := cfg.GetActiveContext(); err == nil && gpctx != nil {
telemetryEnabled = telemetryEnabled && gpctx.Host.String() == "https://gitpod.io"
}
telemetry.Init(telemetryEnabled, cfg.Telemetry.Identity, constants.Version)
telemetry.Init(telemetryEnabled, cfg.Telemetry.Identity, constants.Version.String())
telemetry.RecordCommand(cmd)

if !isVersionCommand(cmd) {
waitForUpdate := selfupdate.Autoupdate(cmd.Context(), cfg)
cmd.PostRunE = func(cmd *cobra.Command, args []string) error {
waitForUpdate()
return nil
}
}

return nil
},
}
Expand Down
12 changes: 11 additions & 1 deletion components/local-app/cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ type CommandTestExpectation struct {
Output string
}

// AddActiveTestContext sets the active context to "test" which makes sure we run against the test HTTP server
func AddActiveTestContext(cfg *config.Config) *config.Config {
cfg.ActiveContext = "test"
return cfg
}

func RunCommandTests(t *testing.T, tests []CommandTest) {
for _, test := range tests {
name := test.Name
Expand Down Expand Up @@ -69,9 +75,13 @@ func RunCommandTests(t *testing.T, tests []CommandTest) {
}
rootTestingOpts.Client = clnt

testurl, err := url.Parse(apisrv.URL)
if err != nil {
t.Fatal(err)
}
test.Config.Contexts = map[string]*config.ConnectionContext{
"test": {
Host: &config.YamlURL{URL: &url.URL{Scheme: "https", Host: "testing"}},
Host: &config.YamlURL{URL: testurl},
Token: "hello world",
},
}
Expand Down
54 changes: 54 additions & 0 deletions components/local-app/cmd/version-update.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright (c) 2023 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License.AGPL.txt in the project root for license information.

package cmd

import (
"context"
"time"

"github.com/gitpod-io/local-app/pkg/config"
"github.com/gitpod-io/local-app/pkg/constants"
"github.com/gitpod-io/local-app/pkg/selfupdate"
"github.com/sagikazarmark/slog-shim"
"github.com/spf13/cobra"
)

var versionUpdateCmd = &cobra.Command{
Use: "update",
Short: "Updates the CLI to the latest version",
RunE: func(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true

dlctx, cancel := context.WithTimeout(cmd.Context(), 30*time.Second)
defer cancel()

cfg := config.FromContext(cmd.Context())
gpctx, err := cfg.GetActiveContext()
if err != nil {
return err
}

mf, err := selfupdate.DownloadManifest(dlctx, gpctx.Host.URL.String())
if err != nil {
return err
}
if !selfupdate.NeedsUpdate(constants.Version, mf) {
slog.Info("already up to date")
return nil
}

slog.Info("updating to latest version " + mf.Version.String())
err = selfupdate.ReplaceSelf(dlctx, mf)
if err != nil {
return err
}

return nil
},
}

func init() {
versionCmd.AddCommand(versionUpdateCmd)
}
67 changes: 67 additions & 0 deletions components/local-app/cmd/version-update_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright (c) 2023 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License.AGPL.txt in the project root for license information.

package cmd

import (
"encoding/json"
"net/http"
"runtime"
"testing"

"github.com/Masterminds/semver/v3"
"github.com/gitpod-io/local-app/pkg/config"
"github.com/gitpod-io/local-app/pkg/constants"
"github.com/gitpod-io/local-app/pkg/selfupdate"
"github.com/opencontainers/go-digest"
)

func TestVersionUpdateCmd(t *testing.T) {
RunCommandTests(t, []CommandTest{
{
Name: "happy path",
Commandline: []string{"version", "update"},
PrepServer: func(mux *http.ServeMux) {
newBinary := []byte("#!/bin/bash\necho hello world")
mux.HandleFunc(selfupdate.GitpodCLIBasePath+"/manifest.json", func(w http.ResponseWriter, r *http.Request) {
mf, err := json.Marshal(selfupdate.Manifest{
Version: semver.MustParse("v9999.0"),
Binaries: []selfupdate.Binary{
{
Filename: "gitpod",
OS: runtime.GOOS,
Arch: runtime.GOARCH,
Digest: digest.FromBytes(newBinary),
},
},
})
if err != nil {
t.Fatal(err)
}
_, _ = w.Write(mf)
})
mux.HandleFunc(selfupdate.GitpodCLIBasePath+"/gitpod", func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write(newBinary)
})
},
Config: AddActiveTestContext(&config.Config{}),
},
{
Name: "no update needed",
Commandline: []string{"version", "update"},
PrepServer: func(mux *http.ServeMux) {
mux.HandleFunc(selfupdate.GitpodCLIBasePath+"/manifest.json", func(w http.ResponseWriter, r *http.Request) {
mf, err := json.Marshal(selfupdate.Manifest{
Version: constants.Version,
})
if err != nil {
t.Fatal(err)
}
_, _ = w.Write(mf)
})
},
Config: AddActiveTestContext(&config.Config{}),
},
})
}
Loading

0 comments on commit e4bc514

Please sign in to comment.