-
Notifications
You must be signed in to change notification settings - Fork 221
/
upgrade.go
193 lines (164 loc) · 5.82 KB
/
upgrade.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
package version
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"github.com/samber/lo"
"github.com/spf13/cobra"
"github.com/superfly/flyctl/terminal"
"golang.org/x/exp/slices"
"github.com/superfly/flyctl/internal/buildinfo"
"github.com/superfly/flyctl/internal/cache"
"github.com/superfly/flyctl/internal/command"
"github.com/superfly/flyctl/internal/update"
"github.com/superfly/flyctl/internal/version"
"github.com/superfly/flyctl/iostreams"
)
func newUpgrade() *cobra.Command {
const (
short = "Checks for available updates and automatically upgrades"
long = `Checks for an update and if one is available, runs the appropriate
command to upgrade the application.`
)
cmd := command.New("upgrade", short, long, runUpgrade)
cmd.Aliases = []string{"update"}
return cmd
}
func runUpgrade(ctx context.Context) error {
release, err := update.LatestRelease(ctx, cache.FromContext(ctx).Channel())
switch {
case err != nil:
return fmt.Errorf("failed determining latest release: %w", err)
case release == nil:
return fmt.Errorf("failed querying latest release information: %w", err)
}
// The API won't return yanked versions, but we don't have a good way
// to yank homebrew releases. If we're under homebrew, we'll validate through the API
if update.IsUnderHomebrew() {
if relErr := update.ValidateRelease(ctx, release.Version); relErr != nil {
return fmt.Errorf("latest version on homebrew is invalid: %s\nplease try again later", relErr)
}
}
latest, err := version.Parse(release.Version)
if err != nil {
return fmt.Errorf("error parsing version: %q, %w", release.Version, err)
}
io := iostreams.FromContext(ctx)
if !latest.Newer(buildinfo.Version()) {
fmt.Fprintf(io.Out, "Already running latest flyctl v%s\n", buildinfo.Version().String())
return nil
}
if !update.CanUpdateThisInstallation() {
return errors.New("cannot update this installation.\nthe environment variable FLYCTL_INSTALL must be set to the installation directory")
}
homebrew := update.IsUnderHomebrew()
if err = update.UpgradeInPlace(ctx, io, release.Prerelease, false); err != nil {
return err
}
err = printVersionUpgrade(ctx, buildinfo.Version(), homebrew)
if err != nil {
terminal.Debugf("Error printing version upgrade: %v", err)
}
return nil
}
// printVersionUpgrade prints "Upgraded flyctl [oldVersion] -> [newVersion]"
func printVersionUpgrade(ctx context.Context, oldVersion version.Version, homebrew bool) error {
var (
io = iostreams.FromContext(ctx)
currentVer version.Version
err error
)
if homebrew {
currentVer, err = getNewVersionHomebrew(ctx)
} else {
currentVer, err = getNewVersionFlyInstaller(ctx)
}
if err != nil {
if strings.Contains(err.Error(), "failed to parse version") {
// This is probably fine, likely a change between the two versions makes
// flyctl <-> flyctl communication incompatible
return nil
} else {
return err
}
}
if currentVer.Equal(oldVersion) {
var source string
if homebrew {
source = "homebrew"
} else {
source = fmt.Sprintf("'%s'", os.Args[0])
}
fmt.Fprintf(io.ErrOut, "Flyctl was upgraded, but the flyctl pointed to by %s is still version %s.\n", source, currentVer.String())
fmt.Fprintf(io.ErrOut, "Please ensure that your PATH is set correctly!")
return nil
}
fmt.Fprintf(io.Out, "Upgraded flyctl v%s -> v%s\n", oldVersion.String(), currentVer.String())
return nil
}
// getNewVersionFlyInstaller queries homebrew for the latest currently installed version of flyctl
// It parses the output of `brew info flyctl --json`
func getNewVersionHomebrew(ctx context.Context) (version.Version, error) {
var ver version.Version
newVersionJson, err := exec.CommandContext(ctx, "brew", "info", "flyctl", "--json").CombinedOutput()
if err != nil {
return ver, fmt.Errorf("failed to query version information from homebrew: %w", err)
}
var parsed []map[string]any
if err = json.Unmarshal(newVersionJson, &parsed); err != nil {
return ver, fmt.Errorf("failed to parse version output from brew: %w", err)
}
versions := lo.Map(parsed, func(def map[string]any, _ int) []*version.Version {
if def["name"] != "flyctl" {
return nil
}
installed, ok := def["installed"].([]any)
if !ok {
return nil
}
return lo.FilterMap(installed, func(defAny any, _ int) (*version.Version, bool) {
v, ok := defAny.(map[string]any)["version"].(string)
if !ok {
return nil, false
}
parsed, err := version.Parse(v)
if err != nil {
return nil, false
}
return &parsed, true
})
})
versionsFlat := lo.Map(lo.Flatten(versions), func(v *version.Version, _ int) version.Version { return *v })
slices.SortFunc(versionsFlat, version.Compare)
if len(versionsFlat) == 0 {
return ver, errors.New("brew reports no installed flyctl version")
}
return versionsFlat[len(versionsFlat)-1], nil
}
// getNewVersionFlyInstaller executes [os.Args[0], "version", "--json"] and parses the output into a semver.Version
func getNewVersionFlyInstaller(ctx context.Context) (version.Version, error) {
var ver version.Version
newVersionJson, err := exec.CommandContext(ctx, os.Args[0], "version", "--json").CombinedOutput()
if err != nil {
return ver, fmt.Errorf("failed to execute new flyctl binary: %w", err)
}
// Parsing into a map instead of the struct directly so that
// small changes in the version struct don't break this.
parsed := map[string]string{}
if err = json.Unmarshal(newVersionJson, &parsed); err != nil {
return ver, fmt.Errorf("failed to parse version of new flyctl binary: %w", err)
}
verStr, ok := parsed["Version"]
if !ok {
return ver, errors.New("failed to parse version of new flyctl binary: field 'Version' not in output of 'fly version --json'")
}
ver, err = version.Parse(verStr)
if err != nil {
return ver, fmt.Errorf("failed to parse version of new flyctl binary: %w", err)
}
return ver, nil
}