Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings
This repository was archived by the owner on Aug 30, 2024. It is now read-only.
/coder-v1-cliPublic archive

feat: Add update command to coder-cli#417

Merged
johnstcn merged 26 commits intomasterfromcianjohnston/autoupdate
Aug 17, 2021
Merged
Show file tree
Hide file tree
Changes from1 commit
Commits
Show all changes
26 commits
Select commitHold shift + click to select a range
d256581
feat: Add update command to coder-cli
johnstcnAug 10, 2021
972847e
internal/cmd/update_test.go: refactor unit tests
johnstcnAug 13, 2021
597afe1
fixup! internal/cmd/update_test.go: refactor unit tests
johnstcnAug 13, 2021
1373e79
internal/cmd/update_test.go: more tests
johnstcnAug 13, 2021
513282a
internal/cmd/update_test.go: create dirs in memfs
johnstcnAug 13, 2021
5bf7f56
internal/cmd/update_test.go: test for windows
johnstcnAug 13, 2021
dabd178
fixup! internal/cmd/update_test.go: test for windows
johnstcnAug 13, 2021
dcfeec1
internal/cmd/update.go: replace semver library
johnstcnAug 13, 2021
0801cfc
internal/cmd/update.go: use /api/private/version instead of sniffing …
johnstcnAug 13, 2021
f6ce76f
gendocs
johnstcnAug 13, 2021
6371084
internal/cmd/update.go: use os.Executable() instead of os.Args[0]
johnstcnAug 13, 2021
bdb998e
internal/cmd/update.go: check path prefixes
johnstcnAug 13, 2021
306686c
lint
johnstcnAug 13, 2021
60a75a1
internal/cmd/update_test.go: assertCLIError helper function for clog.…
johnstcnAug 16, 2021
26984a2
internal/cmd/update.go: allow explicitly specifying version
johnstcnAug 16, 2021
bcaac7b
internal/cmd/update.go: validate we can exec new binary
johnstcnAug 16, 2021
2002876
fixup! internal/cmd/update.go: validate we can exec new binary
johnstcnAug 16, 2021
3ba1bed
internal/cmd/update.go: query github releases api for assets
johnstcnAug 16, 2021
6238053
internal/cmd/update.go: handle copy error from archive
johnstcnAug 17, 2021
24df4f7
internal/cmd/update.go: handle windows-specific behaviours
johnstcnAug 17, 2021
21dd836
fixup! internal/cmd/update.go: handle windows-specific behaviours
johnstcnAug 17, 2021
e064c47
fixup! internal/cmd/update.go: handle windows-specific behaviours
johnstcnAug 17, 2021
eefa2f3
gofmt
johnstcnAug 17, 2021
0e63d2f
internal/cmd/users_test.go: assert CICD user instead of admin
johnstcnAug 17, 2021
60944f0
Revert "internal/cmd/users_test.go: assert CICD user instead of admin"
johnstcnAug 17, 2021
c8943ed
Merge branch 'master' into cianjohnston/autoupdate
johnstcnAug 17, 2021
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: 2 additions & 0 deletionsgo.mod
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ go 1.14
require (
cdr.dev/slog v1.4.1
cdr.dev/wsep v0.0.0-20200728013649-82316a09813f
github.com/blang/semver/v4 v4.0.0
github.com/briandowns/spinner v1.16.0
github.com/cli/safeexec v1.0.0
github.com/fatih/color v1.12.0
Expand All@@ -23,6 +24,7 @@ require (
github.com/pion/webrtc/v3 v3.0.32
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4
github.com/rjeczalik/notify v0.9.2
github.com/spf13/afero v1.6.0
github.com/spf13/cobra v1.2.1
github.com/stretchr/testify v1.7.0
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97
Expand Down
3 changes: 3 additions & 0 deletionsgo.sum
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,6 +69,8 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/briandowns/spinner v1.16.0 h1:DFmp6hEaIx2QXXuqSJmtfSBSAjRmpGiKG6ip2Wm/yOs=
github.com/briandowns/spinner v1.16.0/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
Expand DownExpand Up@@ -376,6 +378,7 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5I
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw=
Expand Down
1 change: 1 addition & 0 deletionsinternal/cmd/cmd.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,6 +38,7 @@ func Make() *cobra.Command {
tagsCmd(),
tokensCmd(),
tunnelCmd(),
updateCmd(),
urlCmd(),
usersCmd(),
workspacesCmd(),
Expand Down
309 changes: 309 additions & 0 deletionsinternal/cmd/update.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
package cmd

import (
"archive/tar"
"archive/zip"
"bufio"
"bytes"
"compress/gzip"
"context"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path"
"runtime"
"strings"
"time"

"cdr.dev/coder-cli/internal/config"
"cdr.dev/coder-cli/internal/version"
"cdr.dev/coder-cli/pkg/clog"
"golang.org/x/xerrors"

"github.com/blang/semver/v4"
"github.com/manifoldco/promptui"
"github.com/spf13/afero"
"github.com/spf13/cobra"
)

// updater updates coder-cli.
type updater struct {
confirmF func(label string) (string, error)
executablePath string
fs afero.Fs
httpClient getter
versionF func() string
}

func updateCmd() *cobra.Command {
var (
force bool
coderURL string
)

cmd := &cobra.Command{
Use: "update",
Short: "Update coder binary",
Long: "Update coder to the version matching a given coder instance.",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
httpClient := &http.Client{
Timeout: 10 * time.Second,
}

updater := &updater{
httpClient: httpClient,
fs: afero.NewOsFs(),
confirmF: defaultConfirm,
versionF: func() string { return version.Version },
executablePath: os.Args[0],
}
return updater.Run(ctx, force, coderURL)
},
}

cmd.Flags().BoolVar(&force, "force", false, "do not prompt for confirmation")
cmd.Flags().StringVar(&coderURL, "coder", "", "coder instance against which to match version")

return cmd
}

type getter interface {
Get(url string) (*http.Response, error)
}

func (u *updater) Run(ctx context.Context, force bool, coderURLString string) error {
// TODO: check under following directories and warn if coder binary is under them:
// * homebrew prefix
// * coder assets root (env CODER_ASSETS_ROOT)

currentBinaryStat, err := u.fs.Stat(u.executablePath)
if err != nil {
return clog.Fatal("preflight: cannot stat current binary", clog.Causef("%s", err))
}

if currentBinaryStat.Mode().Perm()&0222 == 0 {
return clog.Fatal("preflight: missing write permission on current binary")
}

var coderURL *url.URL
if coderURLString == "" {
coderURL, err = getCoderConfigURL()
if err != nil {
return clog.Fatal(
"Unable to automatically determine coder URL",
clog.Causef(err.Error()),
clog.BlankLine,
clog.Tipf("use --coder <url> to specify coder URL"),
)
}
} else {
coderURL, err = url.Parse(coderURLString)
if err != nil {
return clog.Fatal("invalid coder URL", err.Error())
}
}

desiredVersion, err := getAPIVersionUnauthed(u.httpClient, *coderURL)
if err != nil {
return clog.Fatal("fetch api version", clog.Causef(err.Error()))
}

clog.LogInfo(fmt.Sprintf("Coder instance at %q reports version %s", coderURL.String(), desiredVersion.String()))
clog.LogInfo(fmt.Sprintf("Current version of coder-cli is %s", version.Version))

if currentVersion, err := semver.Make(u.versionF()); err == nil {
if desiredVersion.Compare(currentVersion) == 0 {
clog.LogInfo("Up to date!")
return nil
}
}

if !force {
label := fmt.Sprintf("Update coder-cli to version %s", desiredVersion.FinalizeVersion())
if _, err := u.confirmF(label); err != nil {
return clog.Fatal("failed to confirm update", clog.Tipf(`use "--force" to update without confirmation`))
}
}

downloadURL := makeDownloadURL(desiredVersion.FinalizeVersion(), runtime.GOOS, runtime.GOARCH)

var downloadBuf bytes.Buffer
memWriter := bufio.NewWriter(&downloadBuf)

clog.LogInfo("fetching coder-cli from GitHub releases", downloadURL)
resp, err := u.httpClient.Get(downloadURL)
if err != nil {
return clog.Fatal(fmt.Sprintf("failed to fetch URL %s", downloadURL), clog.Causef(err.Error()))
}

if resp.StatusCode != http.StatusOK {
return clog.Fatal("failed to fetch release", clog.Causef("URL %s returned status code %d", downloadURL, resp.StatusCode))
}

if _, err := io.Copy(memWriter, resp.Body); err != nil {
return clog.Fatal(fmt.Sprintf("failed to download %s", downloadURL), clog.Causef(err.Error()))
}

_ = resp.Body.Close()

if err := memWriter.Flush(); err != nil {
return clog.Fatal(fmt.Sprintf("failed to save %s", downloadURL), clog.Causef(err.Error()))
}

// TODO: validate the checksum of the downloaded file. GitHub does not currently provide this information
// and we do not generate them yet.
updatedBinary, err := extractFromArchive("coder", downloadBuf.Bytes())
if err != nil {
return clog.Fatal("failed to extract coder binary from archive", clog.Causef(err.Error()))
}

// We assume the binary is named coder and write it to coder.new
updatedCoderBinaryPath := u.executablePath + ".new"
updatedBin, err := u.fs.OpenFile(updatedCoderBinaryPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, currentBinaryStat.Mode().Perm())
if err != nil {
return clog.Fatal("failed to create file for updated coder binary", clog.Causef(err.Error()))
}

fsWriter := bufio.NewWriter(updatedBin)
if _, err := io.Copy(fsWriter, bytes.NewReader(updatedBinary)); err != nil {
return clog.Fatal("failed to write updated coder binary to disk", clog.Causef(err.Error()))
}

if err := fsWriter.Flush(); err != nil {
return clog.Fatal("failed to persist updated coder binary to disk", clog.Causef(err.Error()))
}

if err = u.fs.Rename(updatedCoderBinaryPath, u.executablePath); err != nil {
return clog.Fatal("failed to update coder binary in-place", clog.Causef(err.Error()))
}

clog.LogSuccess("Updated coder CLI to version " + desiredVersion.FinalizeVersion())
return nil
}

func defaultConfirm(label string) (string, error) {
p := promptui.Prompt{IsConfirm: true, Label: label}
return p.Run()
}

func makeDownloadURL(version, ostype, archetype string) string {
const template = "https://github.com/cdr/coder-cli/releases/download/v%s/coder-cli-%s-%s.%s"
var ext string
switch ostype {
case "linux":
ext = "tar.gz"
default:
ext = ".zip"
}
return fmt.Sprintf(template, version, ostype, archetype, ext)
}

func extractFromArchive(path string, archive []byte) ([]byte, error) {
contentType := http.DetectContentType(archive)
switch contentType {
case "application/zip":
return extractFromZip(path, archive)
case "application/x-gzip":
return extractFromTgz(path, archive)
default:
return nil, xerrors.Errorf("unknown archive type: %s", contentType)
}
}

func extractFromZip(path string, archive []byte) ([]byte, error) {
zipReader, err := zip.NewReader(bytes.NewReader(archive), int64(len(archive)))
if err != nil {
return nil, xerrors.Errorf("failed to open zip archive")
}

var zf *zip.File
for _, f := range zipReader.File {
if f.Name == path {
zf = f
break
}
}
if zf == nil {
return nil, xerrors.Errorf("could not find path %q in zip archive", path)
}

rc, err := zf.Open()
if err != nil {
return nil, xerrors.Errorf("failed to extract path %q from archive", path)
}
defer rc.Close()

var b bytes.Buffer
bw := bufio.NewWriter(&b)
if _, err := io.Copy(bw, rc); err != nil {
return nil, xerrors.Errorf("failed to copy path %q to from archive", path)
}
return b.Bytes(), nil
}

func extractFromTgz(path string, archive []byte) ([]byte, error) {
zr, err := gzip.NewReader(bytes.NewReader(archive))
if err != nil {
return nil, xerrors.Errorf("failed to gunzip archive")
}

tr := tar.NewReader(zr)

var b bytes.Buffer
bw := bufio.NewWriter(&b)
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, xerrors.Errorf("failed to read tar archive: %w", err)
}
fi := hdr.FileInfo()
if fi.Name() == path && fi.Mode().IsRegular() {
_, _ = io.Copy(bw, tr)
break
}
}

return b.Bytes(), nil
}

// getCoderConfigURL reads the currently configured coder URL, returning an empty string if not configured.
func getCoderConfigURL() (*url.URL, error) {
urlString, err := config.URL.Read()
if err != nil {
return nil, err
}
configuredURL, err := url.Parse(strings.TrimSpace(urlString))
if err != nil {
return nil, err
}
return configuredURL, nil
}

// XXX: coder.Client requires an API key, but we may not be logged into the coder instance for which we
// want to determine the version. We don't need an API key to sniff the version header.
func getAPIVersionUnauthed(client getter, baseURL url.URL) (semver.Version, error) {
baseURL.Path = path.Join(baseURL.Path, "/api")
resp, err := client.Get(baseURL.String())
if err != nil {
return semver.Version{}, xerrors.Errorf("get %s: %w", baseURL.String(), err)
}
defer resp.Body.Close()

versionHdr := resp.Header.Get("coder-version")
if versionHdr == "" {
return semver.Version{}, xerrors.Errorf("URL %s response missing coder-version header", baseURL.String())
}

version, err := semver.Parse(versionHdr)
if err != nil {
return semver.Version{}, xerrors.Errorf("parsing coder-version header: %w", err)
}

return version, nil
}
Loading

[8]ページ先頭

©2009-2025 Movatter.jp