github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/update-main.go (about) 1 // Copyright (c) 2015-2022 MinIO, Inc. 2 // 3 // This file is part of MinIO Object Storage stack 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package cmd 19 20 import ( 21 "crypto" 22 "crypto/tls" 23 "encoding/hex" 24 "errors" 25 "fmt" 26 "io" 27 "net" 28 "net/http" 29 "net/url" 30 "os" 31 "path" 32 "path/filepath" 33 "runtime" 34 "strings" 35 "time" 36 37 _ "crypto/sha256" // needed for selfupdate hashers 38 39 "github.com/fatih/color" 40 "github.com/mattn/go-isatty" 41 "github.com/minio/cli" 42 json "github.com/minio/colorjson" 43 "github.com/minio/mc/pkg/probe" 44 "github.com/minio/pkg/v2/env" 45 "github.com/minio/selfupdate" 46 ) 47 48 // Check for new software updates. 49 var updateCmd = cli.Command{ 50 Name: "update", 51 Usage: "update mc to latest release", 52 Action: mainUpdate, 53 OnUsageError: onUsageError, 54 Flags: []cli.Flag{ 55 cli.BoolFlag{ 56 Name: "json", 57 Usage: "enable JSON lines formatted output", 58 }, 59 }, 60 CustomHelpTemplate: `Name: 61 {{.HelpName}} - {{.Usage}} 62 63 USAGE: 64 {{.HelpName}}{{if .VisibleFlags}} [FLAGS]{{end}} 65 {{if .VisibleFlags}} 66 FLAGS: 67 {{range .VisibleFlags}}{{.}} 68 {{end}}{{end}} 69 EXIT STATUS: 70 0 - you are already running the most recent version 71 1 - new update was applied successfully 72 -1 - error in getting update information 73 74 EXAMPLES: 75 1. Check and update mc: 76 {{.Prompt}} {{.HelpName}} 77 `, 78 } 79 80 const ( 81 mcReleaseTagTimeLayout = "2006-01-02T15-04-05Z" 82 mcOSARCH = runtime.GOOS + "-" + runtime.GOARCH 83 mcReleaseURL = "https://dl.min.io/client/mc/release/" + mcOSARCH + "/" 84 85 envMinisignPubKey = "MC_UPDATE_MINISIGN_PUBKEY" 86 ) 87 88 // For windows our files have .exe additionally. 89 var mcReleaseWindowsInfoURL = mcReleaseURL + "mc.exe.sha256sum" 90 91 // mcVersionToReleaseTime - parses a standard official release 92 // mc --version string. 93 // 94 // An official binary's version string is the release time formatted 95 // with RFC3339 (in UTC) - e.g. `2017-09-29T19:16:56Z` 96 func mcVersionToReleaseTime(version string) (releaseTime time.Time, err *probe.Error) { 97 var e error 98 releaseTime, e = time.Parse(time.RFC3339, version) 99 return releaseTime, probe.NewError(e) 100 } 101 102 // releaseTagToReleaseTime - releaseTag to releaseTime 103 func releaseTagToReleaseTime(releaseTag string) (releaseTime time.Time, err *probe.Error) { 104 fields := strings.Split(releaseTag, ".") 105 if len(fields) < 2 || len(fields) > 4 { 106 return releaseTime, probe.NewError(fmt.Errorf("%s is not a valid release tag", releaseTag)) 107 } 108 if fields[0] != "RELEASE" { 109 return releaseTime, probe.NewError(fmt.Errorf("%s is not a valid release tag", releaseTag)) 110 } 111 var e error 112 releaseTime, e = time.Parse(mcReleaseTagTimeLayout, fields[1]) 113 return releaseTime, probe.NewError(e) 114 } 115 116 // getModTime - get the file modification time of `path` 117 func getModTime(path string) (t time.Time, err *probe.Error) { 118 var e error 119 path, e = filepath.EvalSymlinks(path) 120 if e != nil { 121 return t, probe.NewError(fmt.Errorf("Unable to get absolute path of %s. %w", path, e)) 122 } 123 124 // Version is mc non-standard, we will use mc binary's 125 // ModTime as release time. 126 var fi os.FileInfo 127 fi, e = os.Stat(path) 128 if e != nil { 129 return t, probe.NewError(fmt.Errorf("Unable to get ModTime of %s. %w", path, e)) 130 } 131 132 // Return the ModTime 133 return fi.ModTime().UTC(), nil 134 } 135 136 // GetCurrentReleaseTime - returns this process's release time. If it 137 // is official mc --version, parsed version is returned else mc 138 // binary's mod time is returned. 139 func GetCurrentReleaseTime() (releaseTime time.Time, err *probe.Error) { 140 if releaseTime, err = mcVersionToReleaseTime(Version); err == nil { 141 return releaseTime, nil 142 } 143 144 // Looks like version is mc non-standard, we use mc 145 // binary's ModTime as release time: 146 path, e := os.Executable() 147 if e != nil { 148 return releaseTime, probe.NewError(e) 149 } 150 return getModTime(path) 151 } 152 153 // IsDocker - returns if the environment mc is running in docker or 154 // not. The check is a simple file existence check. 155 // 156 // https://github.com/moby/moby/blob/master/daemon/initlayer/setup_unix.go#L25 157 // 158 // "/.dockerenv": "file", 159 func IsDocker() bool { 160 _, e := os.Stat("/.dockerenv") 161 if os.IsNotExist(e) { 162 return false 163 } 164 165 return e == nil 166 } 167 168 // IsDCOS returns true if mc is running in DCOS. 169 func IsDCOS() bool { 170 // http://mesos.apache.org/documentation/latest/docker-containerizer/ 171 // Mesos docker containerizer sets this value 172 return os.Getenv("MESOS_CONTAINER_NAME") != "" 173 } 174 175 // IsKubernetes returns true if MinIO is running in kubernetes. 176 func IsKubernetes() bool { 177 // Kubernetes env used to validate if we are 178 // indeed running inside a kubernetes pod 179 // is KUBERNETES_SERVICE_HOST but in future 180 // we might need to enhance this. 181 return os.Getenv("KUBERNETES_SERVICE_HOST") != "" 182 } 183 184 // IsSourceBuild - returns if this binary is a non-official build from 185 // source code. 186 func IsSourceBuild() bool { 187 _, err := mcVersionToReleaseTime(Version) 188 return err != nil 189 } 190 191 // DO NOT CHANGE USER AGENT STYLE. 192 // The style should be 193 // 194 // mc (<OS>; <ARCH>[; dcos][; kubernetes][; docker][; source]) mc/<VERSION> mc/<RELEASE-TAG> mc/<COMMIT-ID> 195 // 196 // Any change here should be discussed by opening an issue at 197 // https://github.com/minio/mc/issues. 198 func getUserAgent() string { 199 userAgentParts := []string{} 200 // Helper function to concisely append a pair of strings to a 201 // the user-agent slice. 202 uaAppend := func(p, q string) { 203 userAgentParts = append(userAgentParts, p, q) 204 } 205 206 uaAppend("mc (", runtime.GOOS) 207 uaAppend("; ", runtime.GOARCH) 208 if IsDCOS() { 209 uaAppend("; ", "dcos") 210 } 211 if IsKubernetes() { 212 uaAppend("; ", "kubernetes") 213 } 214 if IsDocker() { 215 uaAppend("; ", "docker") 216 } 217 if IsSourceBuild() { 218 uaAppend("; ", "source") 219 } 220 221 uaAppend(") mc/", Version) 222 uaAppend(" mc/", ReleaseTag) 223 uaAppend(" mc/", CommitID) 224 225 return strings.Join(userAgentParts, "") 226 } 227 228 func downloadReleaseURL(releaseChecksumURL string, timeout time.Duration) (content string, err *probe.Error) { 229 req, e := http.NewRequest("GET", releaseChecksumURL, nil) 230 if e != nil { 231 return content, probe.NewError(e) 232 } 233 req.Header.Set("User-Agent", getUserAgent()) 234 235 resp, e := httpClient(timeout).Do(req) 236 if e != nil { 237 return content, probe.NewError(e) 238 } 239 if resp == nil { 240 return content, probe.NewError(fmt.Errorf("No response from server to download URL %s", releaseChecksumURL)) 241 } 242 defer resp.Body.Close() 243 244 if resp.StatusCode != http.StatusOK { 245 return content, probe.NewError(fmt.Errorf("Error downloading URL %s. Response: %v", releaseChecksumURL, resp.Status)) 246 } 247 contentBytes, e := io.ReadAll(resp.Body) 248 if e != nil { 249 return content, probe.NewError(fmt.Errorf("Error reading response. %s", err)) 250 } 251 252 return string(contentBytes), nil 253 } 254 255 // DownloadReleaseData - downloads release data from mc official server. 256 func DownloadReleaseData(customReleaseURL string, timeout time.Duration) (data string, err *probe.Error) { 257 releaseURL := mcReleaseInfoURL 258 if runtime.GOOS == "windows" { 259 releaseURL = mcReleaseWindowsInfoURL 260 } 261 if customReleaseURL != "" { 262 releaseURL = customReleaseURL 263 } 264 return func() (data string, err *probe.Error) { 265 data, err = downloadReleaseURL(releaseURL, timeout) 266 if err == nil { 267 return data, nil 268 } 269 return data, err.Trace(releaseURL) 270 }() 271 } 272 273 // parseReleaseData - parses release info file content fetched from 274 // official mc download server. 275 // 276 // The expected format is a single line with two words like: 277 // 278 // fbe246edbd382902db9a4035df7dce8cb441357d mc.RELEASE.2016-10-07T01-16-39Z 279 // 280 // The second word must be `mc.` appended to a standard release tag. 281 func parseReleaseData(data string) (sha256Hex string, releaseTime time.Time, releaseTag string, err *probe.Error) { 282 fields := strings.Fields(data) 283 if len(fields) != 2 { 284 return sha256Hex, releaseTime, "", probe.NewError(fmt.Errorf("Unknown release data `%s`", data)) 285 } 286 287 sha256Hex = fields[0] 288 releaseInfo := fields[1] 289 290 fields = strings.SplitN(releaseInfo, ".", 2) 291 if len(fields) != 2 { 292 return sha256Hex, releaseTime, "", probe.NewError(fmt.Errorf("Unknown release information `%s`", releaseInfo)) 293 } 294 if fields[0] != "mc" { 295 return sha256Hex, releaseTime, "", probe.NewError(fmt.Errorf("Unknown release `%s`", releaseInfo)) 296 } 297 298 releaseTime, err = releaseTagToReleaseTime(fields[1]) 299 if err != nil { 300 return sha256Hex, releaseTime, fields[1], err.Trace(fields...) 301 } 302 303 return sha256Hex, releaseTime, fields[1], nil 304 } 305 306 func getLatestReleaseTime(customReleaseURL string, timeout time.Duration) (sha256Hex string, releaseTime time.Time, releaseTag string, err *probe.Error) { 307 data, err := DownloadReleaseData(customReleaseURL, timeout) 308 if err != nil { 309 return sha256Hex, releaseTime, releaseTag, err.Trace() 310 } 311 312 return parseReleaseData(data) 313 } 314 315 func getDownloadURL(customReleaseURL, releaseTag string) (downloadURL string) { 316 // Check if we are docker environment, return docker update command 317 if IsDocker() { 318 // Construct release tag name. 319 return fmt.Sprintf("docker pull minio/mc:%s", releaseTag) 320 } 321 322 if customReleaseURL == "" { 323 return mcReleaseURL + "archive/mc." + releaseTag 324 } 325 326 u, e := url.Parse(customReleaseURL) 327 if e != nil { 328 return mcReleaseURL + "archive/mc." + releaseTag 329 } 330 331 u.Path = path.Dir(u.Path) + "/mc." + releaseTag 332 return u.String() 333 } 334 335 func getUpdateInfo(customReleaseURL string, timeout time.Duration) (updateMsg, sha256Hex string, currentReleaseTime, latestReleaseTime time.Time, releaseTag string, err *probe.Error) { 336 currentReleaseTime, err = GetCurrentReleaseTime() 337 if err != nil { 338 return updateMsg, sha256Hex, currentReleaseTime, latestReleaseTime, releaseTag, err.Trace() 339 } 340 341 sha256Hex, latestReleaseTime, releaseTag, err = getLatestReleaseTime(customReleaseURL, timeout) 342 if err != nil { 343 return updateMsg, sha256Hex, currentReleaseTime, latestReleaseTime, releaseTag, err.Trace() 344 } 345 346 var older time.Duration 347 var downloadURL string 348 if latestReleaseTime.After(currentReleaseTime) { 349 older = latestReleaseTime.Sub(currentReleaseTime) 350 downloadURL = getDownloadURL(customReleaseURL, releaseTag) 351 } 352 353 return prepareUpdateMessage(downloadURL, older), sha256Hex, currentReleaseTime, latestReleaseTime, releaseTag, nil 354 } 355 356 var ( 357 // Check if we stderr, stdout are dumb terminals, we do not apply 358 // ansi coloring on dumb terminals. 359 isTerminal = func() bool { 360 return isatty.IsTerminal(os.Stdout.Fd()) && isatty.IsTerminal(os.Stderr.Fd()) 361 } 362 363 colorCyanBold = func() func(a ...interface{}) string { 364 if isTerminal() { 365 color.New(color.FgCyan, color.Bold).SprintFunc() 366 } 367 return fmt.Sprint 368 }() 369 370 colorYellowBold = func() func(format string, a ...interface{}) string { 371 if isTerminal() { 372 return color.New(color.FgYellow, color.Bold).SprintfFunc() 373 } 374 return fmt.Sprintf 375 }() 376 377 colorGreenBold = func() func(format string, a ...interface{}) string { 378 if isTerminal() { 379 return color.New(color.FgGreen, color.Bold).SprintfFunc() 380 } 381 return fmt.Sprintf 382 }() 383 ) 384 385 func getUpdateTransport(timeout time.Duration) http.RoundTripper { 386 var updateTransport http.RoundTripper = &http.Transport{ 387 Proxy: http.ProxyFromEnvironment, 388 DialContext: (&net.Dialer{ 389 Timeout: timeout, 390 KeepAlive: timeout, 391 DualStack: true, 392 }).DialContext, 393 IdleConnTimeout: timeout, 394 TLSHandshakeTimeout: timeout, 395 ExpectContinueTimeout: timeout, 396 TLSClientConfig: &tls.Config{ 397 RootCAs: globalRootCAs, 398 }, 399 DisableCompression: true, 400 } 401 return updateTransport 402 } 403 404 func getUpdateReaderFromURL(u *url.URL, transport http.RoundTripper) (io.ReadCloser, error) { 405 clnt := &http.Client{ 406 Transport: transport, 407 } 408 req, e := http.NewRequest(http.MethodGet, u.String(), nil) 409 if e != nil { 410 return nil, e 411 } 412 req.Header.Set("User-Agent", getUserAgent()) 413 414 resp, e := clnt.Do(req) 415 if e != nil { 416 return nil, e 417 } 418 419 if resp.StatusCode != http.StatusOK { 420 return nil, errors.New(resp.Status) 421 } 422 423 return newProgressReader(resp.Body, "mc", resp.ContentLength), nil 424 } 425 426 func doUpdate(customReleaseURL, sha256Hex string, latestReleaseTime time.Time, releaseTag string, ok bool) (updateStatusMsg string, err *probe.Error) { 427 fmtReleaseTime := latestReleaseTime.Format(mcReleaseTagTimeLayout) 428 if !ok { 429 updateStatusMsg = colorGreenBold("mc update to version %s canceled.", 430 releaseTag) 431 return updateStatusMsg, nil 432 } 433 434 sha256Sum, e := hex.DecodeString(sha256Hex) 435 if e != nil { 436 return updateStatusMsg, probe.NewError(e) 437 } 438 439 u, e := url.Parse(getDownloadURL(customReleaseURL, releaseTag)) 440 if err != nil { 441 return updateStatusMsg, probe.NewError(e) 442 } 443 444 transport := getUpdateTransport(30 * time.Second) 445 446 rc, e := getUpdateReaderFromURL(u, transport) 447 if e != nil { 448 return updateStatusMsg, probe.NewError(e) 449 } 450 defer rc.Close() 451 452 opts := selfupdate.Options{ 453 Hash: crypto.SHA256, 454 Checksum: sha256Sum, 455 } 456 457 minisignPubkey := env.Get(envMinisignPubKey, "") 458 if minisignPubkey != "" { 459 v := selfupdate.NewVerifier() 460 u.Path = path.Dir(u.Path) + "/mc." + releaseTag + ".minisig" 461 if e = v.LoadFromURL(u.String(), minisignPubkey, transport); e != nil { 462 return updateStatusMsg, probe.NewError(e) 463 } 464 opts.Verifier = v 465 } 466 467 if e := opts.CheckPermissions(); e != nil { 468 permErrMsg := fmt.Sprintf(" failed with: %s", e) 469 updateStatusMsg = colorYellowBold("mc update to version RELEASE.%s %s.", 470 fmtReleaseTime, permErrMsg) 471 return updateStatusMsg, nil 472 } 473 474 if e = selfupdate.Apply(rc, opts); e != nil { 475 if re := selfupdate.RollbackError(e); re != nil { 476 rollBackErr := fmt.Sprintf("Failed to rollback from bad update: %v", re) 477 updateStatusMsg = colorYellowBold("mc update to version RELEASE.%s %s.", fmtReleaseTime, rollBackErr) 478 return updateStatusMsg, probe.NewError(e) 479 } 480 481 var pathErr *os.PathError 482 if errors.As(e, &pathErr) { 483 pathErrMsg := fmt.Sprintf("Unable to update the binary at %s: %v", filepath.Dir(pathErr.Path), pathErr.Err) 484 updateStatusMsg = colorYellowBold("mc update to version RELEASE.%s %s.", 485 fmtReleaseTime, pathErrMsg) 486 return updateStatusMsg, nil 487 } 488 489 return colorYellowBold(fmt.Sprintf("Error in mc update to version RELEASE.%s %v.", fmtReleaseTime, e)), nil 490 } 491 492 return colorGreenBold("mc updated to version RELEASE.%s successfully.", fmtReleaseTime), nil 493 } 494 495 type updateMessage struct { 496 Status string `json:"status"` 497 Message string `json:"message"` 498 } 499 500 // String colorized make bucket message. 501 func (s updateMessage) String() string { 502 return s.Message 503 } 504 505 // JSON jsonified make bucket message. 506 func (s updateMessage) JSON() string { 507 s.Status = "success" 508 updateJSONBytes, e := json.MarshalIndent(s, "", " ") 509 fatalIf(probe.NewError(e), "Unable to marshal into JSON.") 510 511 return string(updateJSONBytes) 512 } 513 514 func mainUpdate(ctx *cli.Context) { 515 if len(ctx.Args()) > 1 { 516 showCommandHelpAndExit(ctx, -1) 517 } 518 519 globalQuiet = ctx.Bool("quiet") || ctx.GlobalBool("quiet") 520 globalJSON = ctx.Bool("json") || ctx.GlobalBool("json") 521 522 customReleaseURL := ctx.Args().Get(0) 523 524 updateMsg, sha256Hex, _, latestReleaseTime, releaseTag, err := getUpdateInfo(customReleaseURL, 10*time.Second) 525 if err != nil { 526 errorIf(err, "Unable to update ‘mc’.") 527 os.Exit(-1) 528 } 529 530 // Nothing to update running the latest release. 531 color.New(color.FgGreen, color.Bold) 532 if updateMsg == "" { 533 printMsg(updateMessage{ 534 Status: "success", 535 Message: colorGreenBold("You are already running the most recent version of ‘mc’."), 536 }) 537 os.Exit(0) 538 } 539 540 printMsg(updateMessage{ 541 Status: "success", 542 Message: updateMsg, 543 }) 544 545 // Avoid updating mc development, source builds. 546 if updateMsg != "" { 547 var updateStatusMsg string 548 var err *probe.Error 549 updateStatusMsg, err = doUpdate(customReleaseURL, sha256Hex, latestReleaseTime, releaseTag, true) 550 if err != nil { 551 errorIf(err, "Unable to update ‘mc’.") 552 os.Exit(-1) 553 } 554 printMsg(updateMessage{Status: "success", Message: updateStatusMsg}) 555 os.Exit(1) 556 } 557 }