Skip to content

Commit

Permalink
AUTH-2993 added workers updater logic
Browse files Browse the repository at this point in the history
  • Loading branch information
Dalton committed Oct 1, 2020
1 parent 2c9b736 commit ba4c8d8
Show file tree
Hide file tree
Showing 8 changed files with 768 additions and 30 deletions.
29 changes: 25 additions & 4 deletions cmd/cloudflared/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,16 +81,37 @@ func main() {

tunnel.Init(Version, shutdownC, graceShutdownC) // we need this to support the tunnel sub command...
access.Init(shutdownC, graceShutdownC)
updater.Init(Version)
runApp(app, shutdownC, graceShutdownC)
}

func commands(version func(c *cli.Context)) []*cli.Command {
cmds := []*cli.Command{
{
Name: "update",
Action: cliutil.ErrorHandler(updater.Update),
Usage: "Update the agent if a new version exists",
ArgsUsage: " ",
Name: "update",
Action: cliutil.ErrorHandler(updater.Update),
Usage: "Update the agent if a new version exists",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "beta",
Usage: "specify if you wish to update to the latest beta version",
},
&cli.BoolFlag{
Name: "force",
Usage: "specify if you wish to force an upgrade to the latest version regardless of the current version",
Hidden: true,
},
&cli.BoolFlag{
Name: "staging",
Usage: "specify if you wish to use the staging url for updating",
Hidden: true,
},
&cli.StringFlag{
Name: "version",
Usage: "specify a version you wish to upgrade or downgrade to",
Hidden: false,
},
},
Description: `Looks for a new version on the official download server.
If a new version exists, updates the agent binary and quits.
Otherwise, does nothing.
Expand Down
26 changes: 26 additions & 0 deletions cmd/cloudflared/updater/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package updater

// Version is the functions needed to perform an update
type Version interface {
Apply() error
String() string
}

// Service is the functions to get check for new updates
type Service interface {
Check() (Version, error)
}

const (
// 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)
OSKeyName = "os"

// ArchitectureKeyName is the url parameter key to send to the checkin API for the architecture of the local cloudflared (e.g. amd64, x86)
ArchitectureKeyName = "arch"

// BetaKeyName is the url parameter key to send to the checkin API to signal if the update should be a beta version or not
BetaKeyName = "beta"

// VersionKeyName is the url parameter key to send to the checkin API to specific what version to upgrade or downgrade to
VersionKeyName = "version"
)
78 changes: 55 additions & 23 deletions cmd/cloudflared/updater/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (

"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/logger"
"github.com/equinox-io/equinox"
"github.com/facebookgo/grace/gracenet"
"github.com/pkg/errors"
)
Expand All @@ -25,16 +24,12 @@ const (
noUpdateOnWindowsMessage = "cloudflared will not automatically update on Windows systems."
noUpdateManagedPackageMessage = "cloudflared will not automatically update if installed by a package manager."
isManagedInstallFile = ".installedFromPackageManager"
UpdateURL = "https://update.argotunnel.com"
StagingUpdateURL = "https://staging-update.argotunnel.com"
)

var (
publicKey = []byte(`
-----BEGIN ECDSA PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE4OWZocTVZ8Do/L6ScLdkV+9A0IYMHoOf
dsCmJ/QZ6aw0w9qkkwEpne1Lmo6+0pGexZzFZOH6w5amShn+RXt7qkSid9iWlzGq
EKx0BZogHSor9Wy5VztdFaAaVbsJiCbO
-----END ECDSA PUBLIC KEY-----
`)
version string
)

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

type updateOptions struct {
isBeta bool
isStaging bool
isForced bool
version string
}

type UpdateOutcome struct {
Updated bool
Version string
Expand All @@ -74,29 +76,44 @@ func (uo *UpdateOutcome) noUpdate() bool {
return uo.Error == nil && uo.Updated == false
}

func checkForUpdateAndApply() UpdateOutcome {
var opts equinox.Options
if err := opts.SetPublicKeyPEM(publicKey); err != nil {
func Init(v string) {
version = v
}

func checkForUpdateAndApply(options updateOptions) UpdateOutcome {
cfdPath, err := os.Executable()
if err != nil {
return UpdateOutcome{Error: err}
}

resp, err := equinox.Check(appID, opts)
switch {
case err == equinox.NotAvailableErr:
return UpdateOutcome{}
case err != nil:
url := UpdateURL
if options.isStaging {
url = StagingUpdateURL
}

s := NewWorkersService(version, url, cfdPath, Options{IsBeta: options.isBeta,
IsForced: options.isForced, RequestedVersion: options.version})

v, err := s.Check()
if err != nil {
return UpdateOutcome{Error: err}
}

err = resp.Apply()
//already on the latest version
if v == nil {
return UpdateOutcome{}
}

err = v.Apply()
if err != nil {
return UpdateOutcome{Error: err}
}

return UpdateOutcome{Updated: true, Version: resp.ReleaseVersion}
return UpdateOutcome{Updated: true, Version: v.String()}
}

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

updateOutcome := loggedUpdate(logger)
isBeta := c.Bool("beta")
if isBeta {
logger.Info("cloudflared is set to update to the latest beta version")
}

isStaging := c.Bool("staging")
if isStaging {
logger.Info("cloudflared is set to update from staging")
}

isForced := c.Bool("force")
if isForced {
logger.Info("cloudflared is set to upgrade to the latest publish version regardless of the current version")
}

updateOutcome := loggedUpdate(logger, updateOptions{isBeta: isBeta, isStaging: isStaging, isForced: isForced, version: c.String("version")})
if updateOutcome.Error != nil {
return &statusErr{updateOutcome.Error}
}
Expand All @@ -121,8 +153,8 @@ func Update(_ *cli.Context) error {
}

// Checks for an update and applies it if one is available
func loggedUpdate(logger logger.Service) UpdateOutcome {
updateOutcome := checkForUpdateAndApply()
func loggedUpdate(logger logger.Service, options updateOptions) UpdateOutcome {
updateOutcome := checkForUpdateAndApply(options)
if updateOutcome.Updated {
logger.Infof("cloudflared has been updated to version %s", updateOutcome.Version)
}
Expand Down Expand Up @@ -168,7 +200,7 @@ func (a *AutoUpdater) Run(ctx context.Context) error {
ticker := time.NewTicker(a.configurable.freq)
for {
if a.configurable.enabled {
updateOutcome := loggedUpdate(a.logger)
updateOutcome := loggedUpdate(a.logger, updateOptions{})
if updateOutcome.Updated {
os.Args = append(os.Args, "--is-autoupdated=true")
if IsSysV() {
Expand Down
154 changes: 154 additions & 0 deletions cmd/cloudflared/updater/workers_service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package updater

import (
"encoding/json"
"errors"
"net/http"
"runtime"
"strconv"
"strings"
"time"
)

// Options are the update options supported by the
type Options struct {
// IsBeta is for beta updates to be installed if available
IsBeta bool

// IsForced is to forcibly download the latest version regardless of the current version
IsForced bool

// RequestedVersion is the specific version to upgrade or downgrade to
RequestedVersion string
}

// VersionResponse is the JSON response from the Workers API endpoint
type VersionResponse struct {
URL string `json:"url"`
Version string `json:"version"`
Checksum string `json:"checksum"`
IsCompressed bool `json:"compressed"`
Error string `json:"error"`
}

// WorkersService implements Service.
// It contains everything needed to check in with the WorkersAPI and download and apply the updates
type WorkersService struct {
currentVersion string
url string
targetPath string
opts Options
}

// NewWorkersService creates a new updater Service object.
func NewWorkersService(currentVersion, url, targetPath string, opts Options) Service {
return &WorkersService{
currentVersion: currentVersion,
url: url,
targetPath: targetPath,
opts: opts,
}
}

// Check does a check in with the Workers API to get a new version update
func (s *WorkersService) Check() (Version, error) {
client := &http.Client{
Timeout: time.Second * 5,
}

req, err := http.NewRequest(http.MethodGet, s.url, nil)
q := req.URL.Query()
q.Add(OSKeyName, runtime.GOOS)
q.Add(ArchitectureKeyName, runtime.GOARCH)

if s.opts.IsBeta {
q.Add(BetaKeyName, "true")
}

if s.opts.RequestedVersion != "" {
q.Add(VersionKeyName, s.opts.RequestedVersion)
}

req.URL.RawQuery = q.Encode()
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

var v VersionResponse
if err := json.NewDecoder(resp.Body).Decode(&v); err != nil {
return nil, err
}

if v.Error != "" {
return nil, errors.New(v.Error)
}

if !s.opts.IsForced && !IsNewerVersion(s.currentVersion, v.Version) {
return nil, nil
}

return NewWorkersVersion(v.URL, v.Version, v.Checksum, s.targetPath, v.IsCompressed), nil
}

// IsNewerVersion checks semantic versioning for the latest version
// cloudflared tagging is more of a date than a semantic version,
// but the same comparision logic still holds for major.minor.patch
// e.g. 2020.8.2 is newer than 2020.8.1.
func IsNewerVersion(current string, check string) bool {
if strings.Contains(strings.ToLower(current), "dev") {
return false // dev builds shouldn't update
}

cMajor, cMinor, cPatch, err := SemanticParts(current)
if err != nil {
return false
}

nMajor, nMinor, nPatch, err := SemanticParts(check)
if err != nil {
return false
}

if nMajor > cMajor {
return true
}

if nMajor == cMajor && nMinor > cMinor {
return true
}

if nMajor == cMajor && nMinor == cMinor && nPatch > cPatch {
return true
}
return false
}

// SemanticParts gets the major, minor, and patch version of a semantic version string
// e.g. 3.1.2 would return 3, 1, 2, nil
func SemanticParts(version string) (major int, minor int, patch int, err error) {
major = 0
minor = 0
patch = 0
parts := strings.Split(version, ".")
if len(parts) != 3 {
err = errors.New("invalid version")
return
}
major, err = strconv.Atoi(parts[0])
if err != nil {
return
}

minor, err = strconv.Atoi(parts[1])
if err != nil {
return
}

patch, err = strconv.Atoi(parts[2])
if err != nil {
return
}
return
}
Loading

0 comments on commit ba4c8d8

Please sign in to comment.