Skip to content
This repository was archived by the owner on Dec 18, 2024. It is now read-only.

Commit b6cb1c6

Browse files
committed
kosh update command
This brings in the code that sungo wrote just before he left to restore the self-update functionality from conch-shell. It's been ported to the current codebase (though it doesn't entirely match the current style).
1 parent 0e0ae67 commit b6cb1c6

File tree

4 files changed

+336
-2
lines changed

4 files changed

+336
-2
lines changed

cli/cli.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ func NewApp(c Config) *cli.Cli {
116116
app.Command("rooms", "Work with datacenter rooms", roomsCmd)
117117
app.Command("schema", "Get the server JSON Schema for a given request or response", schemaCmd)
118118
app.Command("user u", "Commands for dealing with the current user (you)", userCmd)
119+
app.Command("update", "commands for updating kosh", updateCmd)
119120
app.Command("validation v", "Work with validations", validationCmd)
120121
app.Command("whoami", "Display details of the current user", whoamiCmd)
121122

@@ -144,8 +145,7 @@ func NewApp(c Config) *cli.Cli {
144145
}
145146
}
146147

147-
config.Debug("Starting App")
148-
config.Info(config)
148+
config.Debug(config)
149149
}
150150

151151
return app

cli/updater.go

+331
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
package cli
2+
3+
import (
4+
"crypto/sha256"
5+
"encoding/hex"
6+
"errors"
7+
"fmt"
8+
"io/ioutil"
9+
"net/http"
10+
"os"
11+
"path/filepath"
12+
"regexp"
13+
"runtime"
14+
"sort"
15+
"strings"
16+
17+
"github.com/blang/semver"
18+
"github.com/dghubble/sling"
19+
cli "github.com/jawher/mow.cli"
20+
)
21+
22+
const (
23+
GhOrg = "joyent"
24+
GhRepo = "kosh"
25+
)
26+
27+
// GithubRelease represents a 'release' for a Github project
28+
type GithubRelease struct {
29+
URL string `json:"html_url"`
30+
TagName string `json:"tag_name"`
31+
SemVer semver.Version `json:"-"` // Will be set to 0.0.0 if no releases are found
32+
Body string `json:"body"`
33+
Name string `json:"name"`
34+
Assets []GithubAsset `json:"assets"`
35+
PreRelease bool `json:"prerelease"`
36+
Upgrade bool `json:"-"`
37+
}
38+
39+
type GithubReleases []GithubRelease
40+
41+
func (g GithubReleases) Len() int { return len(g) }
42+
func (g GithubReleases) Swap(i, j int) { g[i], g[j] = g[j], g[i] }
43+
func (g GithubReleases) Less(i, j int) bool {
44+
iSem := CleanVersion(g[i].TagName)
45+
jSem := CleanVersion(g[j].TagName)
46+
return iSem.GT(jSem) // reversing sort
47+
}
48+
49+
// GithubAsset represents a file inside of a github release
50+
type GithubAsset struct {
51+
URL string `json:"url"`
52+
Name string `json:"name"`
53+
State string `json:"state"`
54+
BrowserDownloadURL string `json:"browser_download_url"`
55+
}
56+
57+
var ErrNoGithubRelease = errors.New("no appropriate github release found")
58+
59+
// LatestGithubRelease returns some fields from the latest Github Release
60+
// that matches our major version
61+
func LatestGithubRelease() (gh GithubRelease, err error) {
62+
releases := make(GithubReleases, 0)
63+
64+
url := fmt.Sprintf(
65+
"https://api.github.com/repos/%s/%s/releases",
66+
GhOrg,
67+
GhRepo,
68+
)
69+
70+
_, err = sling.New().Get(url).Receive(&releases, nil)
71+
72+
if err != nil {
73+
return gh, err
74+
}
75+
76+
sort.Sort(releases)
77+
78+
sem := CleanVersion(config.Version)
79+
80+
for _, r := range releases {
81+
if r.PreRelease {
82+
continue
83+
}
84+
if r.TagName == "" {
85+
continue
86+
}
87+
r.SemVer = CleanVersion(r.TagName)
88+
89+
// Two things are at play here. First, we only care about releases that
90+
// share our major number. This prevents someone from updating from
91+
// v1.42 to v2.0 which might contain breaking changes.
92+
// Second, since we've sorted these in descending order, the first
93+
// release we find with our major number is the largest. We don't need
94+
// to dig any further.
95+
if r.SemVer.Major == sem.Major {
96+
if r.SemVer.GT(sem) {
97+
r.Upgrade = true
98+
}
99+
return r, nil
100+
}
101+
}
102+
103+
return gh, ErrNoGithubRelease
104+
}
105+
106+
func GithubReleasesSince(start semver.Version) GithubReleases {
107+
releases := make(GithubReleases, 0)
108+
109+
diff := make(GithubReleases, 0)
110+
111+
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases", GhOrg, GhRepo)
112+
113+
config.Debug(sling.New().Get(url).Request())
114+
115+
res, err := sling.New().Get(url).Receive(&releases, nil)
116+
config.Debug(res)
117+
118+
if err != nil {
119+
config.Debug(fmt.Sprintf("Error fetching relases: %+v", err))
120+
return diff
121+
}
122+
123+
sort.Sort(releases)
124+
sem := CleanVersion(config.Version)
125+
126+
for _, r := range releases {
127+
if r.PreRelease {
128+
continue
129+
}
130+
if r.TagName == "" {
131+
continue
132+
}
133+
134+
r.SemVer = CleanVersion(r.TagName)
135+
136+
// We will not show changelogs for releases that do not share our major
137+
// version. Since we don't allow users to upgrade across a major
138+
// version, it's silly to show them those changelogs.
139+
if r.SemVer.Major == sem.Major {
140+
if r.SemVer.GT(start) {
141+
diff = append(diff, r)
142+
}
143+
}
144+
}
145+
146+
sort.Sort(diff)
147+
148+
return diff
149+
}
150+
151+
// CleanVersion removes a "v" prefix, and anything after a dash
152+
// For example, pass in v2.99.10-abcde-dirty and get back a semver containing
153+
// 2.29.10
154+
// Why? Git and Semver differ in their notions of what those extra bits mean.
155+
// In Git, they mean "v2.99.10, plus some other stuff that happend". In semver,
156+
// they indicate that this is a prerelease of v2.99.10. Obviously this screws
157+
// up comparisions. This function lets us clean that stuff out so we can get a
158+
// clean comparison
159+
func CleanVersion(version string) semver.Version {
160+
v, err := semver.ParseTolerant(version)
161+
fatalIf(err)
162+
return v
163+
}
164+
165+
func updaterDownloadFile(downloadURL string) (data []byte, err error) {
166+
config.Info(fmt.Sprintf("Downloading '%s'\n", downloadURL))
167+
168+
resp, err := http.Get(downloadURL)
169+
if err != nil {
170+
return data, err
171+
}
172+
173+
if resp.StatusCode != 200 {
174+
return data, fmt.Errorf(
175+
"could not download '%s' (status %d)",
176+
downloadURL,
177+
resp.StatusCode,
178+
)
179+
}
180+
181+
data, err = ioutil.ReadAll(resp.Body)
182+
resp.Body.Close()
183+
184+
return data, err
185+
}
186+
187+
func updateCmd(cmd *cli.Cmd) {
188+
cmd.Command("status", "Verify that we have the most recent revision", updateStatusCmd)
189+
cmd.Command("changelog", "Display the latest changelog", updateChangelogCmd)
190+
cmd.Command("self", "Update the running application to the latest release", updateSelfCmd)
191+
}
192+
193+
func updateStatusCmd(cmd *cli.Cmd) {
194+
cmd.Action = func() {
195+
gh, err := LatestGithubRelease()
196+
if err != nil {
197+
if err == ErrNoGithubRelease {
198+
fmt.Printf("This is %s. No upgrade is available.\n", config.Version)
199+
return
200+
}
201+
fatalIf(err)
202+
}
203+
if gh.Upgrade {
204+
fmt.Printf("This is %s. An upgrade to %s is available\n", config.Version, gh.TagName)
205+
} else {
206+
fmt.Printf("This is %s. No upgrade is available.\n", config.Version)
207+
}
208+
}
209+
}
210+
211+
func updateChangelogCmd(cmd *cli.Cmd) {
212+
cmd.Action = func() {
213+
releases := GithubReleasesSince(CleanVersion(config.Version))
214+
if len(releases) == 0 {
215+
fatalIf(fmt.Errorf("no changes found since %s", config.Version))
216+
}
217+
218+
sort.Sort(sort.Reverse(releases))
219+
220+
for _, gh := range releases {
221+
// I'm not going to try and fully sanitize the output
222+
// for a shell environment but removing the markdown
223+
// backticks seems like a no-brainer for safety.
224+
// TODO (perigrin) render with glow so we don't have to worry about markdown beign interpreted
225+
re := regexp.MustCompile("`")
226+
body := gh.Body
227+
re.ReplaceAllLiteralString(body, "'")
228+
fmt.Printf("# Version %s:\n\n", gh.TagName)
229+
fmt.Println(gh.Body)
230+
fmt.Printf("\n---\n\n")
231+
}
232+
}
233+
}
234+
235+
func updateSelfCmd(cmd *cli.Cmd) {
236+
force := cmd.BoolOpt("force", false, "Update the binary even if it appears we are on the current release")
237+
238+
cmd.Action = func() {
239+
gh, err := LatestGithubRelease()
240+
if err != nil {
241+
if err == ErrNoGithubRelease {
242+
fatalIf(errors.New("no upgrade available"))
243+
}
244+
fatalIf(err)
245+
}
246+
247+
if !*force {
248+
if !gh.Upgrade {
249+
fatalIf(errors.New("no upgrade required"))
250+
}
251+
}
252+
config.Info(fmt.Sprintf("Attempting to upgrade from %s to %s...\n", config.Version, gh.SemVer))
253+
config.Info(fmt.Sprintf("Detected OS to be '%s' and arch to be '%s'\n", runtime.GOOS, runtime.GOARCH))
254+
255+
// What platform are we on?
256+
// XXX lookingFor := fmt.Sprintf("kosh-%s-%s", runtime.GOOS, runtime.GOARCH)
257+
lookingFor := fmt.Sprintf("conch-%s-%s", runtime.GOOS, runtime.GOARCH)
258+
downloadURL := ""
259+
260+
// Is this a supported platform
261+
for _, a := range gh.Assets {
262+
if a.Name == lookingFor {
263+
downloadURL = a.BrowserDownloadURL
264+
}
265+
}
266+
if downloadURL == "" {
267+
fatalIf(fmt.Errorf("could not find an appropriate binary for %s-%s", runtime.GOOS, runtime.GOARCH))
268+
}
269+
270+
//***** Download the binary
271+
conchBin, err := updaterDownloadFile(downloadURL)
272+
fatalIf(err)
273+
274+
//***** Verify checksum
275+
276+
// This assumes our build system is being sensible about file names.
277+
// At time of writing, it is.
278+
shaURL := downloadURL + ".sha256"
279+
shaBin, err := updaterDownloadFile(shaURL)
280+
fatalIf(err)
281+
282+
// The checksum file looks like "thisisahexstring ./kosh-os-arch"
283+
bits := strings.Split(string(shaBin[:]), " ")
284+
remoteSum := bits[0]
285+
286+
config.Info(fmt.Sprintf("Server-side SHA256 sum: %s\n", remoteSum))
287+
288+
h := sha256.New()
289+
h.Write(conchBin)
290+
sum := hex.EncodeToString(h.Sum(nil))
291+
292+
config.Info(fmt.Sprintf("SHA256 sum of downloaded binary: %s\n", sum))
293+
294+
if sum == remoteSum {
295+
config.Info("SHA256 checksums match\n")
296+
} else {
297+
fatalIf(fmt.Errorf("!!! SHA of downloaded file does not match the provided SHA sum: '%s' != '%s'", sum, remoteSum))
298+
}
299+
300+
//***** Write out the binary
301+
binPath, err := os.Executable()
302+
fatalIf(err)
303+
304+
fullPath, err := filepath.EvalSymlinks(binPath)
305+
fatalIf(err)
306+
307+
config.Info(fmt.Sprintf("Detected local binary path: %s\n", fullPath))
308+
309+
existingStat, err := os.Lstat(fullPath)
310+
fatalIf(err)
311+
312+
// On sensible operating systems, we can't open and write to our
313+
// own binary, because it's in use. We can, however, move a file
314+
// into that place.
315+
316+
newPath := fmt.Sprintf("%s-%s", fullPath, gh.SemVer)
317+
config.Info(fmt.Sprintf("Writing to temp file '%s'\n", newPath))
318+
319+
if err := ioutil.WriteFile(newPath, conchBin, existingStat.Mode()); err != nil {
320+
fatalIf(err)
321+
}
322+
323+
config.Info(fmt.Sprintf("Renaming '%s' to '%s'\n", newPath, fullPath))
324+
325+
if err := os.Rename(newPath, fullPath); err != nil {
326+
fatalIf(err)
327+
}
328+
329+
config.Info(fmt.Sprintf("Successfully upgraded from %s to %s\n", config.Version, gh.SemVer))
330+
}
331+
}

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/joyent/kosh
33
go 1.15
44

55
require (
6+
github.com/blang/semver v3.5.1+incompatible
67
github.com/dghubble/sling v1.3.0
78
github.com/dnaeon/go-vcr v1.0.1
89
github.com/gofrs/uuid v3.2.0+incompatible

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
2+
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
13
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
24
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
35
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

0 commit comments

Comments
 (0)