cloudflare/cloudflared
Publicmirrored from https://github.com/cloudflare/cloudflaredAvailable
cmd/cloudflared/updater/update.go
300lines · modecode
| 1 | package updater |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "fmt" |
| 6 | "os" |
| 7 | "path/filepath" |
| 8 | "runtime" |
| 9 | "time" |
| 10 | |
| 11 | "github.com/facebookgo/grace/gracenet" |
| 12 | "github.com/rs/zerolog" |
| 13 | "github.com/urfave/cli/v2" |
| 14 | "golang.org/x/crypto/ssh/terminal" |
| 15 | |
| 16 | "github.com/cloudflare/cloudflared/config" |
| 17 | "github.com/cloudflare/cloudflared/logger" |
| 18 | ) |
| 19 | |
| 20 | const ( |
| 21 | DefaultCheckUpdateFreq = time.Hour * 24 |
| 22 | noUpdateInShellMessage = "cloudflared will not automatically update when run from the shell. To enable auto-updates, run cloudflared as a service: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/run-tunnel/run-as-service" |
| 23 | noUpdateOnWindowsMessage = "cloudflared will not automatically update on Windows systems." |
| 24 | noUpdateManagedPackageMessage = "cloudflared will not automatically update if installed by a package manager." |
| 25 | isManagedInstallFile = ".installedFromPackageManager" |
| 26 | UpdateURL = "https://update.argotunnel.com" |
| 27 | StagingUpdateURL = "https://staging-update.argotunnel.com" |
| 28 | |
| 29 | LogFieldVersion = "version" |
| 30 | ) |
| 31 | |
| 32 | var ( |
| 33 | version string |
| 34 | ) |
| 35 | |
| 36 | // BinaryUpdated implements ExitCoder interface, the app will exit with status code 11 |
| 37 | // https://pkg.go.dev/github.com/urfave/cli/v2?tab=doc#ExitCoder |
| 38 | type statusSuccess struct { |
| 39 | newVersion string |
| 40 | } |
| 41 | |
| 42 | func (u *statusSuccess) Error() string { |
| 43 | return fmt.Sprintf("cloudflared has been updated to version %s", u.newVersion) |
| 44 | } |
| 45 | |
| 46 | func (u *statusSuccess) ExitCode() int { |
| 47 | return 11 |
| 48 | } |
| 49 | |
| 50 | // UpdateErr implements ExitCoder interface, the app will exit with status code 10 |
| 51 | type statusErr struct { |
| 52 | err error |
| 53 | } |
| 54 | |
| 55 | func (e *statusErr) Error() string { |
| 56 | return fmt.Sprintf("failed to update cloudflared: %v", e.err) |
| 57 | } |
| 58 | |
| 59 | func (e *statusErr) ExitCode() int { |
| 60 | return 10 |
| 61 | } |
| 62 | |
| 63 | type updateOptions struct { |
| 64 | updateDisabled bool |
| 65 | isBeta bool |
| 66 | isStaging bool |
| 67 | isForced bool |
| 68 | intendedVersion string |
| 69 | } |
| 70 | |
| 71 | type UpdateOutcome struct { |
| 72 | Updated bool |
| 73 | Version string |
| 74 | UserMessage string |
| 75 | Error error |
| 76 | } |
| 77 | |
| 78 | func (uo *UpdateOutcome) noUpdate() bool { |
| 79 | return uo.Error == nil && uo.Updated == false |
| 80 | } |
| 81 | |
| 82 | func Init(v string) { |
| 83 | version = v |
| 84 | } |
| 85 | |
| 86 | func CheckForUpdate(options updateOptions) (CheckResult, error) { |
| 87 | cfdPath, err := os.Executable() |
| 88 | if err != nil { |
| 89 | return nil, err |
| 90 | } |
| 91 | |
| 92 | url := UpdateURL |
| 93 | if options.isStaging { |
| 94 | url = StagingUpdateURL |
| 95 | } |
| 96 | |
| 97 | s := NewWorkersService(version, url, cfdPath, Options{IsBeta: options.isBeta, |
| 98 | IsForced: options.isForced, RequestedVersion: options.intendedVersion}) |
| 99 | |
| 100 | return s.Check() |
| 101 | } |
| 102 | |
| 103 | func applyUpdate(options updateOptions, update CheckResult) UpdateOutcome { |
| 104 | if update.Version() == "" || options.updateDisabled { |
| 105 | return UpdateOutcome{UserMessage: update.UserMessage()} |
| 106 | } |
| 107 | |
| 108 | err := update.Apply() |
| 109 | if err != nil { |
| 110 | return UpdateOutcome{Error: err} |
| 111 | } |
| 112 | |
| 113 | return UpdateOutcome{Updated: true, Version: update.Version(), UserMessage: update.UserMessage()} |
| 114 | } |
| 115 | |
| 116 | // Update is the handler for the update command from the command line |
| 117 | func Update(c *cli.Context) error { |
| 118 | log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog) |
| 119 | |
| 120 | if wasInstalledFromPackageManager() { |
| 121 | log.Error().Msg("cloudflared was installed by a package manager. Please update using the same method.") |
| 122 | return nil |
| 123 | } |
| 124 | |
| 125 | isBeta := c.Bool("beta") |
| 126 | if isBeta { |
| 127 | log.Info().Msg("cloudflared is set to update to the latest beta version") |
| 128 | } |
| 129 | |
| 130 | isStaging := c.Bool("staging") |
| 131 | if isStaging { |
| 132 | log.Info().Msg("cloudflared is set to update from staging") |
| 133 | } |
| 134 | |
| 135 | isForced := c.Bool("force") |
| 136 | if isForced { |
| 137 | log.Info().Msg("cloudflared is set to upgrade to the latest publish version regardless of the current version") |
| 138 | } |
| 139 | |
| 140 | updateOutcome := loggedUpdate(log, updateOptions{ |
| 141 | updateDisabled: false, |
| 142 | isBeta: isBeta, |
| 143 | isStaging: isStaging, |
| 144 | isForced: isForced, |
| 145 | intendedVersion: c.String("version"), |
| 146 | }) |
| 147 | if updateOutcome.Error != nil { |
| 148 | return &statusErr{updateOutcome.Error} |
| 149 | } |
| 150 | |
| 151 | if updateOutcome.noUpdate() { |
| 152 | log.Info().Str(LogFieldVersion, updateOutcome.Version).Msg("cloudflared is up to date") |
| 153 | return nil |
| 154 | } |
| 155 | |
| 156 | return &statusSuccess{newVersion: updateOutcome.Version} |
| 157 | } |
| 158 | |
| 159 | // Checks for an update and applies it if one is available |
| 160 | func loggedUpdate(log *zerolog.Logger, options updateOptions) UpdateOutcome { |
| 161 | checkResult, err := CheckForUpdate(options) |
| 162 | if err != nil { |
| 163 | log.Err(err).Msg("update check failed") |
| 164 | return UpdateOutcome{Error: err} |
| 165 | } |
| 166 | |
| 167 | updateOutcome := applyUpdate(options, checkResult) |
| 168 | if updateOutcome.Updated { |
| 169 | log.Info().Str(LogFieldVersion, updateOutcome.Version).Msg("cloudflared has been updated") |
| 170 | } |
| 171 | if updateOutcome.Error != nil { |
| 172 | log.Err(updateOutcome.Error).Msg("update failed to apply") |
| 173 | } |
| 174 | |
| 175 | return updateOutcome |
| 176 | } |
| 177 | |
| 178 | // AutoUpdater periodically checks for new version of cloudflared. |
| 179 | type AutoUpdater struct { |
| 180 | configurable *configurable |
| 181 | listeners *gracenet.Net |
| 182 | updateConfigChan chan *configurable |
| 183 | log *zerolog.Logger |
| 184 | } |
| 185 | |
| 186 | // AutoUpdaterConfigurable is the attributes of AutoUpdater that can be reconfigured during runtime |
| 187 | type configurable struct { |
| 188 | enabled bool |
| 189 | freq time.Duration |
| 190 | } |
| 191 | |
| 192 | func NewAutoUpdater(updateDisabled bool, freq time.Duration, listeners *gracenet.Net, log *zerolog.Logger) *AutoUpdater { |
| 193 | return &AutoUpdater{ |
| 194 | configurable: createUpdateConfig(updateDisabled, freq, log), |
| 195 | listeners: listeners, |
| 196 | updateConfigChan: make(chan *configurable), |
| 197 | log: log, |
| 198 | } |
| 199 | } |
| 200 | |
| 201 | func createUpdateConfig(updateDisabled bool, freq time.Duration, log *zerolog.Logger) *configurable { |
| 202 | if isAutoupdateEnabled(log, updateDisabled, freq) { |
| 203 | log.Info().Dur("autoupdateFreq", freq).Msg("Autoupdate frequency is set") |
| 204 | return &configurable{ |
| 205 | enabled: true, |
| 206 | freq: freq, |
| 207 | } |
| 208 | } else { |
| 209 | return &configurable{ |
| 210 | enabled: false, |
| 211 | freq: DefaultCheckUpdateFreq, |
| 212 | } |
| 213 | } |
| 214 | } |
| 215 | |
| 216 | func (a *AutoUpdater) Run(ctx context.Context) error { |
| 217 | ticker := time.NewTicker(a.configurable.freq) |
| 218 | for { |
| 219 | updateOutcome := loggedUpdate(a.log, updateOptions{updateDisabled: !a.configurable.enabled}) |
| 220 | if updateOutcome.Updated { |
| 221 | Init(updateOutcome.Version) |
| 222 | if IsSysV() { |
| 223 | // SysV doesn't have a mechanism to keep service alive, we have to restart the process |
| 224 | a.log.Info().Msg("Restarting service managed by SysV...") |
| 225 | pid, err := a.listeners.StartProcess() |
| 226 | if err != nil { |
| 227 | a.log.Err(err).Msg("Unable to restart server automatically") |
| 228 | return &statusErr{err: err} |
| 229 | } |
| 230 | // stop old process after autoupdate. Otherwise we create a new process |
| 231 | // after each update |
| 232 | a.log.Info().Msgf("PID of the new process is %d", pid) |
| 233 | } |
| 234 | return &statusSuccess{newVersion: updateOutcome.Version} |
| 235 | } else if updateOutcome.UserMessage != "" { |
| 236 | a.log.Warn().Msg(updateOutcome.UserMessage) |
| 237 | } |
| 238 | |
| 239 | select { |
| 240 | case <-ctx.Done(): |
| 241 | return ctx.Err() |
| 242 | case newConfigurable := <-a.updateConfigChan: |
| 243 | ticker.Stop() |
| 244 | a.configurable = newConfigurable |
| 245 | ticker = time.NewTicker(a.configurable.freq) |
| 246 | // Check if there is new version of cloudflared after receiving new AutoUpdaterConfigurable |
| 247 | case <-ticker.C: |
| 248 | } |
| 249 | } |
| 250 | } |
| 251 | |
| 252 | // Update is the method to pass new AutoUpdaterConfigurable to a running AutoUpdater. It is safe to be called concurrently |
| 253 | func (a *AutoUpdater) Update(updateDisabled bool, newFreq time.Duration) { |
| 254 | a.updateConfigChan <- createUpdateConfig(updateDisabled, newFreq, a.log) |
| 255 | } |
| 256 | |
| 257 | func isAutoupdateEnabled(log *zerolog.Logger, updateDisabled bool, updateFreq time.Duration) bool { |
| 258 | if !supportAutoUpdate(log) { |
| 259 | return false |
| 260 | } |
| 261 | return !updateDisabled && updateFreq != 0 |
| 262 | } |
| 263 | |
| 264 | func supportAutoUpdate(log *zerolog.Logger) bool { |
| 265 | if runtime.GOOS == "windows" { |
| 266 | log.Info().Msg(noUpdateOnWindowsMessage) |
| 267 | return false |
| 268 | } |
| 269 | |
| 270 | if wasInstalledFromPackageManager() { |
| 271 | log.Info().Msg(noUpdateManagedPackageMessage) |
| 272 | return false |
| 273 | } |
| 274 | |
| 275 | if isRunningFromTerminal() { |
| 276 | log.Info().Msg(noUpdateInShellMessage) |
| 277 | return false |
| 278 | } |
| 279 | return true |
| 280 | } |
| 281 | |
| 282 | func wasInstalledFromPackageManager() bool { |
| 283 | ok, _ := config.FileExists(filepath.Join(config.DefaultUnixConfigLocation, isManagedInstallFile)) |
| 284 | return ok |
| 285 | } |
| 286 | |
| 287 | func isRunningFromTerminal() bool { |
| 288 | return terminal.IsTerminal(int(os.Stdout.Fd())) |
| 289 | } |
| 290 | |
| 291 | func IsSysV() bool { |
| 292 | if runtime.GOOS != "linux" { |
| 293 | return false |
| 294 | } |
| 295 | |
| 296 | if _, err := os.Stat("/run/systemd/system"); err == nil { |
| 297 | return false |
| 298 | } |
| 299 | return true |
| 300 | } |
| 301 | |