-
Notifications
You must be signed in to change notification settings - Fork 221
/
update.go
502 lines (420 loc) · 10.8 KB
/
update.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
package update
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
"github.com/cli/safeexec"
"github.com/morikuni/aec"
"github.com/superfly/flyctl/terminal"
"github.com/superfly/flyctl/internal/buildinfo"
"github.com/superfly/flyctl/internal/cmdutil"
"github.com/superfly/flyctl/internal/env"
"github.com/superfly/flyctl/iostreams"
)
type Release struct {
Version string `yaml:"version"`
Prerelease bool `yaml:"prerelease"`
DownloadURL string `yaml:"download_url" json:"download_url"`
Timestamp time.Time `yaml:"timestamp"`
}
// Check reports whether update checks should take place.
func Check() bool {
switch {
case env.IsTruthy("FLY_UPDATE_CHECK"):
return true
case env.IsTruthy("FLY_NO_UPDATE_CHECK"):
return false
case env.IsSet("CODESPACES"):
return false
case !buildinfo.IsRelease(), env.IsCI():
return false
case !cmdutil.IsTerminal(os.Stdout), !cmdutil.IsTerminal(os.Stderr):
return false
default:
return true
}
}
type InvalidReleaseError struct {
status int
msg string
}
func (i InvalidReleaseError) Error() string {
return i.msg
}
func (i InvalidReleaseError) StatusCode() int {
return i.status
}
// memoized values for ValidateRelease
var (
_validatedReleases = map[string]error{}
_validatedReleaseLock sync.Mutex
)
// ValidateRelease reports whether the given release is valid via an API call.
// If the version is invalid, the error will be an InvalidReleaseError.
// Note that other errors may be returned if the API call fails.
func ValidateRelease(ctx context.Context, version string) (err error) {
_validatedReleaseLock.Lock()
defer _validatedReleaseLock.Unlock()
if version[0] == 'v' {
version = version[1:]
}
if err, ok := _validatedReleases[version]; ok {
return err
}
defer func() {
_validatedReleases[version] = err
}()
updateUrl := fmt.Sprintf("https://api.fly.io/app/flyctl_validate/v%s", version)
req, err := http.NewRequestWithContext(ctx, "GET", updateUrl, nil)
if err != nil {
return err
}
req.Header.Add("Accept", "text/plain")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer func() {
err := resp.Body.Close()
if err != nil {
terminal.Debugf("error closing response body: %s", err)
}
}()
if resp.StatusCode != http.StatusOK {
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
return &InvalidReleaseError{
status: resp.StatusCode,
msg: string(body),
}
}
return nil
}
// LatestRelease reports the latest release for the given channel.
func LatestRelease(ctx context.Context, channel string) (*Release, error) {
channel = translateChannelForRails(channel)
// If running under homebrew, use the homebrew API to get the latest release
if IsUnderHomebrew() {
return latestHomebrewRelease(ctx, channel)
}
updateUrl := fmt.Sprintf("https://api.fly.io/app/flyctl_releases/%s/%s/%s", runtime.GOOS, runtime.GOARCH, channel)
req, err := http.NewRequestWithContext(ctx, "GET", updateUrl, nil)
if err != nil {
return nil, err
}
req.Header.Add("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer func() {
err := resp.Body.Close()
if err != nil {
terminal.Debugf("error closing response body: %s", err)
}
}()
var release Release
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
return &release, err
}
return &release, nil
}
func latestHomebrewRelease(ctx context.Context, channel string) (*Release, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "https://formulae.brew.sh/api/formula/flyctl.json", nil)
if err != nil {
return nil, err
}
req.Header.Add("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer func() {
err := resp.Body.Close()
if err != nil {
terminal.Debugf("error closing response body: %s", err)
}
}()
var brewResp struct {
Versions struct {
Stable string `json:"stable"`
} `json:"versions"`
}
if err := json.NewDecoder(resp.Body).Decode(&brewResp); err != nil {
return nil, err
}
return &Release{
Version: brewResp.Versions.Stable,
}, nil
}
var errBrewNotFound = errors.New("command 'brew' not found")
// Use brewBinDir()
var _brewBinDir memoize[string]
func brewBinDir() (string, error) {
return _brewBinDir.Get(func() (string, error) {
brewExe, err := safeexec.LookPath("brew")
if err != nil {
return "", errBrewNotFound
}
brewPrefixBytes, err := exec.Command(brewExe, "--prefix").Output()
if err != nil {
return "", err
}
brewBinPrefix := filepath.Join(strings.TrimSpace(string(brewPrefixBytes)), "bin") + string(filepath.Separator)
return brewBinPrefix, nil
})
}
// Use IsUnderHomebrew()
var _isUnderHomebrew memoize[bool]
// IsUnderHomebrew reports whether the fly binary was found under the Homebrew
// prefix.
func IsUnderHomebrew() bool {
if runtime.GOOS == "windows" {
return false
}
val, err := _isUnderHomebrew.Get(func() (bool, error) {
flyBinary, err := os.Executable()
if err != nil {
return false, err
}
brewBinPrefix, err := brewBinDir()
if err != nil {
return false, err
}
return strings.HasPrefix(flyBinary, brewBinPrefix), nil
})
if err != nil {
return false
}
return val
}
func upgradeCommand(prerelease bool) string {
if IsUnderHomebrew() {
return "brew upgrade flyctl"
}
if runtime.GOOS == "windows" {
cmd := "iwr https://fly.io/install.ps1 -useb | iex"
if prerelease {
cmd = "$v=\"pre\"; " + cmd
}
return cmd
} else {
cmd := "curl -L \"https://fly.io/install.sh\" | sh"
if prerelease {
cmd = cmd + " -s pre"
}
return cmd
}
}
func UpgradeInPlace(ctx context.Context, io *iostreams.IOStreams, prelease, silent bool) error {
if runtime.GOOS == "windows" {
if err := renameCurrentBinaries(); err != nil {
return err
}
}
if IsUnderHomebrew() {
brewExe, err := safeexec.LookPath("brew")
if err == nil {
err = exec.Command(brewExe, "update").Run()
}
if err != nil {
terminal.Debugf("error updating homebrew cache: %s", err)
}
}
var shellToUse string
switchToUse := "-c"
ok := false
if runtime.GOOS != "windows" {
shellToUse, ok = os.LookupEnv("SHELL")
}
if !ok {
if runtime.GOOS == "windows" {
// pwsh.exe is the name of the PowerShell executable from 6.0+
// powershell.exe is locked to 5.1 forever
if commandInPath("pwsh.exe") {
shellToUse = "pwsh.exe"
switchToUse = "-Command"
} else {
shellToUse = "powershell.exe"
switchToUse = "-Command"
}
} else {
shellToUse = "/bin/bash"
}
}
command := upgradeCommand(prelease)
cmd := exec.Command(shellToUse, switchToUse, command)
if !silent {
fmt.Fprintf(io.ErrOut, "Running automatic upgrade [%s]\n", command)
cmd.Stdout = io.Out
cmd.Stderr = io.ErrOut
cmd.Stdin = io.In
}
err := cmd.Run()
if err != nil {
return err
}
// Remove the line that says `Run 'flyctl --help' to get started`
if !IsUnderHomebrew() && io.IsInteractive() && !silent {
builder := aec.EmptyBuilder
str := builder.Up(1).EraseLine(aec.EraseModes.All).ANSI
fmt.Fprint(io.ErrOut, str.String())
}
return nil
}
func GetCurrentBinaryPath() (string, error) {
if IsUnderHomebrew() {
brewBinPrefix, err := brewBinDir()
if err != nil {
return "", err
}
homebrewFlyctl := filepath.Join(brewBinPrefix, "flyctl")
if _, err := os.Stat(homebrewFlyctl); err == nil {
return homebrewFlyctl, nil
}
// Not linked (?), use fallback method
}
binPath, err := exec.LookPath(os.Args[0])
if err != nil {
return "", err
}
binPath, err = filepath.EvalSymlinks(binPath)
if err != nil {
return "", err
}
return binPath, nil
}
func CanUpdateThisInstallation() bool {
if IsUnderHomebrew() {
return true
}
binaryPath, err := GetCurrentBinaryPath()
if err != nil {
return false
}
installDir := os.Getenv("FLYCTL_INSTALL")
if installDir == "" {
homeDir, err := os.UserHomeDir()
if err != nil {
return false
}
installDir = filepath.Join(homeDir, ".fly")
}
installDirRealpath, err := filepath.EvalSymlinks(installDir)
if err == nil {
installDir = installDirRealpath
}
return strings.HasPrefix(binaryPath, installDir+string(filepath.Separator))
}
// Relaunch only returns on error
func Relaunch(ctx context.Context, silent bool) error {
io := iostreams.FromContext(ctx)
if !silent {
// Divide between update logging and the output from the actual command the user is running
fmt.Fprint(io.Out, "\n----\n\n")
}
// Wait a bit for the update to take effect.
// Windows seemed to need this for whatever reason.
time.Sleep(400 * time.Millisecond)
binPath, err := GetCurrentBinaryPath()
if err != nil {
return err
}
terminal.Debugf("relaunching %s, found at %s\n", os.Args[0], binPath)
cmd := exec.Command(binPath, os.Args[1:]...)
cmd.Stdout = io.Out
cmd.Stderr = io.ErrOut
cmd.Stdin = io.In
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, "FLY_NO_UPDATE_CHECK=1")
if err := cmd.Start(); err != nil {
return err
}
if err := cmd.Wait(); err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
os.Exit(exitErr.ExitCode())
}
return err
}
os.Exit(0)
return nil
}
func commandInPath(command string) bool {
for _, dir := range filepath.SplitList(os.Getenv("PATH")) {
path := filepath.Join(dir, command)
if _, err := os.Stat(path); err == nil {
return true
}
}
return false
}
// can't replace binary on windows, need to move
func renameCurrentBinaries() error {
binaries, err := currentWindowsBinaries()
if err != nil {
return err
}
for _, p := range binaries {
if err := os.Rename(p, p+".old"); err != nil {
return err
}
}
return nil
}
func currentWindowsBinaries() ([]string, error) {
execPath, err := os.Executable()
if err != nil {
return nil, err
}
canonicalPath, err := filepath.EvalSymlinks(execPath)
if err != nil {
return nil, err
}
return []string{
canonicalPath,
filepath.Join(filepath.Dir(canonicalPath), "wintun.dll"),
}, nil
}
// BackgroundUpdate begins an update in the background.
func BackgroundUpdate() error {
binPath, err := exec.LookPath(os.Args[0])
if err != nil {
return err
}
terminal.Debugf("launching `%s version update` with binary %s\n", os.Args[0], binPath)
cmd := exec.Command(binPath, "version", "upgrade")
cmd.Stdout = nil
cmd.Stderr = nil
cmd.Stdin = nil
if err := cmd.Start(); err != nil {
return err
}
return nil
}
func NormalizeChannel(channel string) string {
channel = strings.ToLower(channel)
return channel
}
// Old install code conflates channels and versions. This fixes it so
// "stable" maps to "latest" while preserving prerelease behavior.
// This will get removed once we're using the new flypkgs API.
func translateChannelForRails(channel string) string {
switch channel {
case "pre", "prerelease":
return "pre"
default:
return "latest"
}
}