cloudflare/cloudflared

Public

mirrored from https://github.com/cloudflare/cloudflaredAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
2021.10.5

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

cmd/cloudflared/updater/update.go

300lines · modecode

1package updater
2
3import (
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
20const (
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
32var (
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
38type statusSuccess struct {
39 newVersion string
40}
41
42func (u *statusSuccess) Error() string {
43 return fmt.Sprintf("cloudflared has been updated to version %s", u.newVersion)
44}
45
46func (u *statusSuccess) ExitCode() int {
47 return 11
48}
49
50// UpdateErr implements ExitCoder interface, the app will exit with status code 10
51type statusErr struct {
52 err error
53}
54
55func (e *statusErr) Error() string {
56 return fmt.Sprintf("failed to update cloudflared: %v", e.err)
57}
58
59func (e *statusErr) ExitCode() int {
60 return 10
61}
62
63type updateOptions struct {
64 updateDisabled bool
65 isBeta bool
66 isStaging bool
67 isForced bool
68 intendedVersion string
69}
70
71type UpdateOutcome struct {
72 Updated bool
73 Version string
74 UserMessage string
75 Error error
76}
77
78func (uo *UpdateOutcome) noUpdate() bool {
79 return uo.Error == nil && uo.Updated == false
80}
81
82func Init(v string) {
83 version = v
84}
85
86func 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
103func 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
117func 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
160func 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.
179type 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
187type configurable struct {
188 enabled bool
189 freq time.Duration
190}
191
192func 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
201func 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
216func (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
253func (a *AutoUpdater) Update(updateDisabled bool, newFreq time.Duration) {
254 a.updateConfigChan <- createUpdateConfig(updateDisabled, newFreq, a.log)
255}
256
257func 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
264func 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
282func wasInstalledFromPackageManager() bool {
283 ok, _ := config.FileExists(filepath.Join(config.DefaultUnixConfigLocation, isManagedInstallFile))
284 return ok
285}
286
287func isRunningFromTerminal() bool {
288 return terminal.IsTerminal(int(os.Stdout.Fd()))
289}
290
291func 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