Skip to content

Commit ba4c8d8

Browse files
author
Dalton
committed
AUTH-2993 added workers updater logic
1 parent 2c9b736 commit ba4c8d8

File tree

8 files changed

+768
-30
lines changed

8 files changed

+768
-30
lines changed

cmd/cloudflared/main.go

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,16 +81,37 @@ func main() {
8181

8282
tunnel.Init(Version, shutdownC, graceShutdownC) // we need this to support the tunnel sub command...
8383
access.Init(shutdownC, graceShutdownC)
84+
updater.Init(Version)
8485
runApp(app, shutdownC, graceShutdownC)
8586
}
8687

8788
func commands(version func(c *cli.Context)) []*cli.Command {
8889
cmds := []*cli.Command{
8990
{
90-
Name: "update",
91-
Action: cliutil.ErrorHandler(updater.Update),
92-
Usage: "Update the agent if a new version exists",
93-
ArgsUsage: " ",
91+
Name: "update",
92+
Action: cliutil.ErrorHandler(updater.Update),
93+
Usage: "Update the agent if a new version exists",
94+
Flags: []cli.Flag{
95+
&cli.BoolFlag{
96+
Name: "beta",
97+
Usage: "specify if you wish to update to the latest beta version",
98+
},
99+
&cli.BoolFlag{
100+
Name: "force",
101+
Usage: "specify if you wish to force an upgrade to the latest version regardless of the current version",
102+
Hidden: true,
103+
},
104+
&cli.BoolFlag{
105+
Name: "staging",
106+
Usage: "specify if you wish to use the staging url for updating",
107+
Hidden: true,
108+
},
109+
&cli.StringFlag{
110+
Name: "version",
111+
Usage: "specify a version you wish to upgrade or downgrade to",
112+
Hidden: false,
113+
},
114+
},
94115
Description: `Looks for a new version on the official download server.
95116
If a new version exists, updates the agent binary and quits.
96117
Otherwise, does nothing.

cmd/cloudflared/updater/service.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package updater
2+
3+
// Version is the functions needed to perform an update
4+
type Version interface {
5+
Apply() error
6+
String() string
7+
}
8+
9+
// Service is the functions to get check for new updates
10+
type Service interface {
11+
Check() (Version, error)
12+
}
13+
14+
const (
15+
// OSKeyName is the url parameter key to send to the checkin API for the operating system of the local cloudflared (e.g. windows, darwin, linux)
16+
OSKeyName = "os"
17+
18+
// ArchitectureKeyName is the url parameter key to send to the checkin API for the architecture of the local cloudflared (e.g. amd64, x86)
19+
ArchitectureKeyName = "arch"
20+
21+
// BetaKeyName is the url parameter key to send to the checkin API to signal if the update should be a beta version or not
22+
BetaKeyName = "beta"
23+
24+
// VersionKeyName is the url parameter key to send to the checkin API to specific what version to upgrade or downgrade to
25+
VersionKeyName = "version"
26+
)

cmd/cloudflared/updater/update.go

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313

1414
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
1515
"github.com/cloudflare/cloudflared/logger"
16-
"github.com/equinox-io/equinox"
1716
"github.com/facebookgo/grace/gracenet"
1817
"github.com/pkg/errors"
1918
)
@@ -25,16 +24,12 @@ const (
2524
noUpdateOnWindowsMessage = "cloudflared will not automatically update on Windows systems."
2625
noUpdateManagedPackageMessage = "cloudflared will not automatically update if installed by a package manager."
2726
isManagedInstallFile = ".installedFromPackageManager"
27+
UpdateURL = "https://update.argotunnel.com"
28+
StagingUpdateURL = "https://staging-update.argotunnel.com"
2829
)
2930

3031
var (
31-
publicKey = []byte(`
32-
-----BEGIN ECDSA PUBLIC KEY-----
33-
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE4OWZocTVZ8Do/L6ScLdkV+9A0IYMHoOf
34-
dsCmJ/QZ6aw0w9qkkwEpne1Lmo6+0pGexZzFZOH6w5amShn+RXt7qkSid9iWlzGq
35-
EKx0BZogHSor9Wy5VztdFaAaVbsJiCbO
36-
-----END ECDSA PUBLIC KEY-----
37-
`)
32+
version string
3833
)
3934

4035
// BinaryUpdated implements ExitCoder interface, the app will exit with status code 11
@@ -64,6 +59,13 @@ func (e *statusErr) ExitCode() int {
6459
return 10
6560
}
6661

62+
type updateOptions struct {
63+
isBeta bool
64+
isStaging bool
65+
isForced bool
66+
version string
67+
}
68+
6769
type UpdateOutcome struct {
6870
Updated bool
6971
Version string
@@ -74,29 +76,44 @@ func (uo *UpdateOutcome) noUpdate() bool {
7476
return uo.Error == nil && uo.Updated == false
7577
}
7678

77-
func checkForUpdateAndApply() UpdateOutcome {
78-
var opts equinox.Options
79-
if err := opts.SetPublicKeyPEM(publicKey); err != nil {
79+
func Init(v string) {
80+
version = v
81+
}
82+
83+
func checkForUpdateAndApply(options updateOptions) UpdateOutcome {
84+
cfdPath, err := os.Executable()
85+
if err != nil {
8086
return UpdateOutcome{Error: err}
8187
}
8288

83-
resp, err := equinox.Check(appID, opts)
84-
switch {
85-
case err == equinox.NotAvailableErr:
86-
return UpdateOutcome{}
87-
case err != nil:
89+
url := UpdateURL
90+
if options.isStaging {
91+
url = StagingUpdateURL
92+
}
93+
94+
s := NewWorkersService(version, url, cfdPath, Options{IsBeta: options.isBeta,
95+
IsForced: options.isForced, RequestedVersion: options.version})
96+
97+
v, err := s.Check()
98+
if err != nil {
8899
return UpdateOutcome{Error: err}
89100
}
90101

91-
err = resp.Apply()
102+
//already on the latest version
103+
if v == nil {
104+
return UpdateOutcome{}
105+
}
106+
107+
err = v.Apply()
92108
if err != nil {
93109
return UpdateOutcome{Error: err}
94110
}
95111

96-
return UpdateOutcome{Updated: true, Version: resp.ReleaseVersion}
112+
return UpdateOutcome{Updated: true, Version: v.String()}
97113
}
98114

99-
func Update(_ *cli.Context) error {
115+
// Update is the handler for the update command from the command line
116+
func Update(c *cli.Context) error {
100117
logger, err := logger.New()
101118
if err != nil {
102119
return errors.Wrap(err, "error setting up logger")
@@ -107,7 +124,22 @@ func Update(_ *cli.Context) error {
107124
return nil
108125
}
109126

110-
updateOutcome := loggedUpdate(logger)
127+
isBeta := c.Bool("beta")
128+
if isBeta {
129+
logger.Info("cloudflared is set to update to the latest beta version")
130+
}
131+
132+
isStaging := c.Bool("staging")
133+
if isStaging {
134+
logger.Info("cloudflared is set to update from staging")
135+
}
136+
137+
isForced := c.Bool("force")
138+
if isForced {
139+
logger.Info("cloudflared is set to upgrade to the latest publish version regardless of the current version")
140+
}
141+
142+
updateOutcome := loggedUpdate(logger, updateOptions{isBeta: isBeta, isStaging: isStaging, isForced: isForced, version: c.String("version")})
111143
if updateOutcome.Error != nil {
112144
return &statusErr{updateOutcome.Error}
113145
}
@@ -121,8 +153,8 @@ func Update(_ *cli.Context) error {
121153
}
122154

123155
// Checks for an update and applies it if one is available
124-
func loggedUpdate(logger logger.Service) UpdateOutcome {
125-
updateOutcome := checkForUpdateAndApply()
156+
func loggedUpdate(logger logger.Service, options updateOptions) UpdateOutcome {
157+
updateOutcome := checkForUpdateAndApply(options)
126158
if updateOutcome.Updated {
127159
logger.Infof("cloudflared has been updated to version %s", updateOutcome.Version)
128160
}
@@ -168,7 +200,7 @@ func (a *AutoUpdater) Run(ctx context.Context) error {
168200
ticker := time.NewTicker(a.configurable.freq)
169201
for {
170202
if a.configurable.enabled {
171-
updateOutcome := loggedUpdate(a.logger)
203+
updateOutcome := loggedUpdate(a.logger, updateOptions{})
172204
if updateOutcome.Updated {
173205
os.Args = append(os.Args, "--is-autoupdated=true")
174206
if IsSysV() {
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package updater
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"net/http"
7+
"runtime"
8+
"strconv"
9+
"strings"
10+
"time"
11+
)
12+
13+
// Options are the update options supported by the
14+
type Options struct {
15+
// IsBeta is for beta updates to be installed if available
16+
IsBeta bool
17+
18+
// IsForced is to forcibly download the latest version regardless of the current version
19+
IsForced bool
20+
21+
// RequestedVersion is the specific version to upgrade or downgrade to
22+
RequestedVersion string
23+
}
24+
25+
// VersionResponse is the JSON response from the Workers API endpoint
26+
type VersionResponse struct {
27+
URL string `json:"url"`
28+
Version string `json:"version"`
29+
Checksum string `json:"checksum"`
30+
IsCompressed bool `json:"compressed"`
31+
Error string `json:"error"`
32+
}
33+
34+
// WorkersService implements Service.
35+
// It contains everything needed to check in with the WorkersAPI and download and apply the updates
36+
type WorkersService struct {
37+
currentVersion string
38+
url string
39+
targetPath string
40+
opts Options
41+
}
42+
43+
// NewWorkersService creates a new updater Service object.
44+
func NewWorkersService(currentVersion, url, targetPath string, opts Options) Service {
45+
return &WorkersService{
46+
currentVersion: currentVersion,
47+
url: url,
48+
targetPath: targetPath,
49+
opts: opts,
50+
}
51+
}
52+
53+
// Check does a check in with the Workers API to get a new version update
54+
func (s *WorkersService) Check() (Version, error) {
55+
client := &http.Client{
56+
Timeout: time.Second * 5,
57+
}
58+
59+
req, err := http.NewRequest(http.MethodGet, s.url, nil)
60+
q := req.URL.Query()
61+
q.Add(OSKeyName, runtime.GOOS)
62+
q.Add(ArchitectureKeyName, runtime.GOARCH)
63+
64+
if s.opts.IsBeta {
65+
q.Add(BetaKeyName, "true")
66+
}
67+
68+
if s.opts.RequestedVersion != "" {
69+
q.Add(VersionKeyName, s.opts.RequestedVersion)
70+
}
71+
72+
req.URL.RawQuery = q.Encode()
73+
resp, err := client.Do(req)
74+
if err != nil {
75+
return nil, err
76+
}
77+
defer resp.Body.Close()
78+
79+
var v VersionResponse
80+
if err := json.NewDecoder(resp.Body).Decode(&v); err != nil {
81+
return nil, err
82+
}
83+
84+
if v.Error != "" {
85+
return nil, errors.New(v.Error)
86+
}
87+
88+
if !s.opts.IsForced && !IsNewerVersion(s.currentVersion, v.Version) {
89+
return nil, nil
90+
}
91+
92+
return NewWorkersVersion(v.URL, v.Version, v.Checksum, s.targetPath, v.IsCompressed), nil
93+
}
94+
95+
// IsNewerVersion checks semantic versioning for the latest version
96+
// cloudflared tagging is more of a date than a semantic version,
97+
// but the same comparision logic still holds for major.minor.patch
98+
// e.g. 2020.8.2 is newer than 2020.8.1.
99+
func IsNewerVersion(current string, check string) bool {
100+
if strings.Contains(strings.ToLower(current), "dev") {
101+
return false // dev builds shouldn't update
102+
}
103+
104+
cMajor, cMinor, cPatch, err := SemanticParts(current)
105+
if err != nil {
106+
return false
107+
}
108+
109+
nMajor, nMinor, nPatch, err := SemanticParts(check)
110+
if err != nil {
111+
return false
112+
}
113+
114+
if nMajor > cMajor {
115+
return true
116+
}
117+
118+
if nMajor == cMajor && nMinor > cMinor {
119+
return true
120+
}
121+
122+
if nMajor == cMajor && nMinor == cMinor && nPatch > cPatch {
123+
return true
124+
}
125+
return false
126+
}
127+
128+
// SemanticParts gets the major, minor, and patch version of a semantic version string
129+
// e.g. 3.1.2 would return 3, 1, 2, nil
130+
func SemanticParts(version string) (major int, minor int, patch int, err error) {
131+
major = 0
132+
minor = 0
133+
patch = 0
134+
parts := strings.Split(version, ".")
135+
if len(parts) != 3 {
136+
err = errors.New("invalid version")
137+
return
138+
}
139+
major, err = strconv.Atoi(parts[0])
140+
if err != nil {
141+
return
142+
}
143+
144+
minor, err = strconv.Atoi(parts[1])
145+
if err != nil {
146+
return
147+
}
148+
149+
patch, err = strconv.Atoi(parts[2])
150+
if err != nil {
151+
return
152+
}
153+
return
154+
}

0 commit comments

Comments
 (0)