2018-10-08 19:20:28 +00:00
package updater
2018-05-01 23:45:06 +00:00
import (
2019-06-18 16:47:29 +00:00
"context"
2020-04-28 00:55:27 +00:00
"fmt"
2020-11-25 06:55:13 +00:00
"github.com/rs/zerolog"
2018-05-01 23:45:06 +00:00
"os"
2020-07-08 14:42:04 +00:00
"path/filepath"
2018-05-01 23:45:06 +00:00
"runtime"
"time"
2020-08-05 10:49:53 +00:00
"github.com/urfave/cli/v2"
2018-05-01 23:45:06 +00:00
"golang.org/x/crypto/ssh/terminal"
2020-07-08 14:42:04 +00:00
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
2020-04-29 20:51:32 +00:00
"github.com/cloudflare/cloudflared/logger"
2018-05-01 23:45:06 +00:00
"github.com/facebookgo/grace/gracenet"
)
const (
2020-07-08 14:42:04 +00:00
DefaultCheckUpdateFreq = time . Hour * 24
noUpdateInShellMessage = "cloudflared will not automatically update when run from the shell. To enable auto-updates, run cloudflared as a service: https://developers.cloudflare.com/argo-tunnel/reference/service/"
noUpdateOnWindowsMessage = "cloudflared will not automatically update on Windows systems."
noUpdateManagedPackageMessage = "cloudflared will not automatically update if installed by a package manager."
isManagedInstallFile = ".installedFromPackageManager"
2020-08-12 16:16:14 +00:00
UpdateURL = "https://update.argotunnel.com"
StagingUpdateURL = "https://staging-update.argotunnel.com"
2018-05-01 23:45:06 +00:00
)
2018-10-08 19:20:28 +00:00
var (
2020-08-12 16:16:14 +00:00
version string
2018-10-08 19:20:28 +00:00
)
2018-05-01 23:45:06 +00:00
2020-04-28 00:55:27 +00:00
// BinaryUpdated implements ExitCoder interface, the app will exit with status code 11
2020-08-05 10:49:53 +00:00
// https://pkg.go.dev/github.com/urfave/cli/v2?tab=doc#ExitCoder
2020-04-28 00:55:27 +00:00
type statusSuccess struct {
newVersion string
}
func ( u * statusSuccess ) Error ( ) string {
return fmt . Sprintf ( "cloudflared has been updated to version %s" , u . newVersion )
}
func ( u * statusSuccess ) ExitCode ( ) int {
return 11
}
// UpdateErr implements ExitCoder interface, the app will exit with status code 10
type statusErr struct {
err error
}
func ( e * statusErr ) Error ( ) string {
return fmt . Sprintf ( "failed to update cloudflared: %v" , e . err )
}
func ( e * statusErr ) ExitCode ( ) int {
return 10
}
2020-08-12 16:16:14 +00:00
type updateOptions struct {
isBeta bool
isStaging bool
isForced bool
version string
}
2019-02-14 22:26:33 +00:00
type UpdateOutcome struct {
2018-05-01 23:45:06 +00:00
Updated bool
Version string
Error error
}
2019-02-14 22:26:33 +00:00
func ( uo * UpdateOutcome ) noUpdate ( ) bool {
2020-06-16 22:20:09 +00:00
return uo . Error == nil && uo . Updated == false
2019-02-14 22:26:33 +00:00
}
2020-08-12 16:16:14 +00:00
func Init ( v string ) {
version = v
}
func checkForUpdateAndApply ( options updateOptions ) UpdateOutcome {
cfdPath , err := os . Executable ( )
if err != nil {
2019-02-14 22:26:33 +00:00
return UpdateOutcome { Error : err }
2018-05-01 23:45:06 +00:00
}
2020-08-12 16:16:14 +00:00
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 {
2019-02-14 22:26:33 +00:00
return UpdateOutcome { Error : err }
2018-05-01 23:45:06 +00:00
}
2020-08-12 16:16:14 +00:00
//already on the latest version
if v == nil {
return UpdateOutcome { }
}
err = v . Apply ( )
2018-05-01 23:45:06 +00:00
if err != nil {
2019-02-14 22:26:33 +00:00
return UpdateOutcome { Error : err }
2018-05-01 23:45:06 +00:00
}
2020-08-12 16:16:14 +00:00
return UpdateOutcome { Updated : true , Version : v . String ( ) }
2018-05-01 23:45:06 +00:00
}
2020-08-12 16:16:14 +00:00
// Update is the handler for the update command from the command line
func Update ( c * cli . Context ) error {
2020-11-25 06:55:13 +00:00
log := logger . CreateLoggerFromContext ( c , logger . EnableTerminalLog )
2020-04-29 20:51:32 +00:00
2020-07-08 14:42:04 +00:00
if wasInstalledFromPackageManager ( ) {
2020-11-25 06:55:13 +00:00
log . Error ( ) . Msg ( "cloudflared was installed by a package manager. Please update using the same method." )
2020-07-08 14:42:04 +00:00
return nil
}
2020-08-12 16:16:14 +00:00
isBeta := c . Bool ( "beta" )
if isBeta {
2020-11-25 06:55:13 +00:00
log . Info ( ) . Msg ( "cloudflared is set to update to the latest beta version" )
2020-08-12 16:16:14 +00:00
}
isStaging := c . Bool ( "staging" )
if isStaging {
2020-11-25 06:55:13 +00:00
log . Info ( ) . Msg ( "cloudflared is set to update from staging" )
2020-08-12 16:16:14 +00:00
}
isForced := c . Bool ( "force" )
if isForced {
2020-11-25 06:55:13 +00:00
log . Info ( ) . Msg ( "cloudflared is set to upgrade to the latest publish version regardless of the current version" )
2020-08-12 16:16:14 +00:00
}
2020-11-25 06:55:13 +00:00
updateOutcome := loggedUpdate ( log , updateOptions { isBeta : isBeta , isStaging : isStaging , isForced : isForced , version : c . String ( "version" ) } )
2019-02-14 22:26:33 +00:00
if updateOutcome . Error != nil {
2020-04-28 00:55:27 +00:00
return & statusErr { updateOutcome . Error }
2019-02-14 22:26:33 +00:00
}
if updateOutcome . noUpdate ( ) {
2020-11-25 06:55:13 +00:00
log . Info ( ) . Msgf ( "cloudflared is up to date (%s)" , updateOutcome . Version )
2020-04-28 00:55:27 +00:00
return nil
2018-05-01 23:45:06 +00:00
}
2019-02-14 22:26:33 +00:00
2020-04-28 00:55:27 +00:00
return & statusSuccess { newVersion : updateOutcome . Version }
2018-05-01 23:45:06 +00:00
}
2019-02-14 22:26:33 +00:00
// Checks for an update and applies it if one is available
2020-11-25 06:55:13 +00:00
func loggedUpdate ( log * zerolog . Logger , options updateOptions ) UpdateOutcome {
2020-08-12 16:16:14 +00:00
updateOutcome := checkForUpdateAndApply ( options )
2019-02-14 22:26:33 +00:00
if updateOutcome . Updated {
2020-11-25 06:55:13 +00:00
log . Info ( ) . Msgf ( "cloudflared has been updated to version %s" , updateOutcome . Version )
2018-05-01 23:45:06 +00:00
}
2019-02-14 22:26:33 +00:00
if updateOutcome . Error != nil {
2020-11-25 06:55:13 +00:00
log . Error ( ) . Msgf ( "update check failed: %s" , updateOutcome . Error )
2018-05-01 23:45:06 +00:00
}
2019-02-14 22:26:33 +00:00
return updateOutcome
2018-05-01 23:45:06 +00:00
}
2019-06-18 16:47:29 +00:00
// AutoUpdater periodically checks for new version of cloudflared.
type AutoUpdater struct {
configurable * configurable
listeners * gracenet . Net
updateConfigChan chan * configurable
2020-11-25 06:55:13 +00:00
log * zerolog . Logger
2019-06-18 16:47:29 +00:00
}
// AutoUpdaterConfigurable is the attributes of AutoUpdater that can be reconfigured during runtime
type configurable struct {
enabled bool
freq time . Duration
}
2020-11-25 06:55:13 +00:00
func NewAutoUpdater ( freq time . Duration , listeners * gracenet . Net , log * zerolog . Logger ) * AutoUpdater {
2019-06-18 16:47:29 +00:00
updaterConfigurable := & configurable {
enabled : true ,
freq : freq ,
}
if freq == 0 {
updaterConfigurable . enabled = false
updaterConfigurable . freq = DefaultCheckUpdateFreq
}
return & AutoUpdater {
configurable : updaterConfigurable ,
listeners : listeners ,
updateConfigChan : make ( chan * configurable ) ,
2020-11-25 06:55:13 +00:00
log : log ,
2019-06-18 16:47:29 +00:00
}
}
func ( a * AutoUpdater ) Run ( ctx context . Context ) error {
ticker := time . NewTicker ( a . configurable . freq )
for {
if a . configurable . enabled {
2020-11-25 06:55:13 +00:00
updateOutcome := loggedUpdate ( a . log , updateOptions { } )
2019-06-18 16:47:29 +00:00
if updateOutcome . Updated {
2020-04-28 00:55:27 +00:00
if IsSysV ( ) {
// SysV doesn't have a mechanism to keep service alive, we have to restart the process
2020-11-25 06:55:13 +00:00
a . log . Info ( ) . Msg ( "Restarting service managed by SysV..." )
2020-04-28 00:55:27 +00:00
pid , err := a . listeners . StartProcess ( )
if err != nil {
2020-11-25 06:55:13 +00:00
a . log . Error ( ) . Msgf ( "Unable to restart server automatically: %s" , err )
2020-04-28 00:55:27 +00:00
return & statusErr { err : err }
}
// stop old process after autoupdate. Otherwise we create a new process
// after each update
2020-11-25 06:55:13 +00:00
a . log . Info ( ) . Msgf ( "PID of the new process is %d" , pid )
2019-06-18 16:47:29 +00:00
}
2020-04-28 00:55:27 +00:00
return & statusSuccess { newVersion : updateOutcome . Version }
2019-06-18 16:47:29 +00:00
}
}
select {
case <- ctx . Done ( ) :
return ctx . Err ( )
case newConfigurable := <- a . updateConfigChan :
ticker . Stop ( )
a . configurable = newConfigurable
ticker = time . NewTicker ( a . configurable . freq )
// Check if there is new version of cloudflared after receiving new AutoUpdaterConfigurable
case <- ticker . C :
}
}
}
// Update is the method to pass new AutoUpdaterConfigurable to a running AutoUpdater. It is safe to be called concurrently
func ( a * AutoUpdater ) Update ( newFreq time . Duration ) {
newConfigurable := & configurable {
enabled : true ,
freq : newFreq ,
}
// A ero duration means autoupdate is disabled
if newFreq == 0 {
newConfigurable . enabled = false
newConfigurable . freq = DefaultCheckUpdateFreq
}
a . updateConfigChan <- newConfigurable
}
2020-11-25 06:55:13 +00:00
func IsAutoupdateEnabled ( c * cli . Context , log * zerolog . Logger ) bool {
if ! SupportAutoUpdate ( log ) {
2019-06-18 16:47:29 +00:00
return false
}
return ! c . Bool ( "no-autoupdate" ) && c . Duration ( "autoupdate-freq" ) != 0
}
2020-11-25 06:55:13 +00:00
func SupportAutoUpdate ( log * zerolog . Logger ) bool {
2018-05-01 23:45:06 +00:00
if runtime . GOOS == "windows" {
2020-11-25 06:55:13 +00:00
log . Info ( ) . Msg ( noUpdateOnWindowsMessage )
2018-05-01 23:45:06 +00:00
return false
}
2020-07-08 14:42:04 +00:00
if wasInstalledFromPackageManager ( ) {
2020-11-25 06:55:13 +00:00
log . Info ( ) . Msg ( noUpdateManagedPackageMessage )
2020-07-08 14:42:04 +00:00
return false
}
2018-05-01 23:45:06 +00:00
if isRunningFromTerminal ( ) {
2020-11-25 06:55:13 +00:00
log . Info ( ) . Msg ( noUpdateInShellMessage )
2018-05-01 23:45:06 +00:00
return false
}
2019-06-18 16:47:29 +00:00
return true
2018-05-01 23:45:06 +00:00
}
2020-07-08 14:42:04 +00:00
func wasInstalledFromPackageManager ( ) bool {
ok , _ := config . FileExists ( filepath . Join ( config . DefaultUnixConfigLocation , isManagedInstallFile ) )
return ok
}
2018-05-01 23:45:06 +00:00
func isRunningFromTerminal ( ) bool {
return terminal . IsTerminal ( int ( os . Stdout . Fd ( ) ) )
}
2020-04-28 00:55:27 +00:00
func IsSysV ( ) bool {
if runtime . GOOS != "linux" {
return false
}
if _ , err := os . Stat ( "/run/systemd/system" ) ; err == nil {
return false
}
return true
}