github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/update.go (about) 1 // Copyright (c) 2015-2021 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 "bufio" 22 "crypto" 23 "crypto/tls" 24 "encoding/hex" 25 "errors" 26 "fmt" 27 "io" 28 "net/http" 29 "net/url" 30 "os" 31 "path" 32 "path/filepath" 33 "runtime" 34 "strings" 35 "sync/atomic" 36 "time" 37 38 "github.com/klauspost/compress/zstd" 39 xhttp "github.com/minio/minio/internal/http" 40 "github.com/minio/minio/internal/logger" 41 "github.com/minio/pkg/v2/env" 42 xnet "github.com/minio/pkg/v2/net" 43 "github.com/minio/selfupdate" 44 gopsutilcpu "github.com/shirou/gopsutil/v3/cpu" 45 "github.com/valyala/bytebufferpool" 46 ) 47 48 const ( 49 envMinisignPubKey = "MINIO_UPDATE_MINISIGN_PUBKEY" 50 updateTimeout = 10 * time.Second 51 ) 52 53 // For windows our files have .exe additionally. 54 var minioReleaseWindowsInfoURL = MinioReleaseURL + "minio.exe.sha256sum" 55 56 // minioVersionToReleaseTime - parses a standard official release 57 // MinIO version string. 58 // 59 // An official binary's version string is the release time formatted 60 // with RFC3339 (in UTC) - e.g. `2017-09-29T19:16:56Z` 61 func minioVersionToReleaseTime(version string) (releaseTime time.Time, err error) { 62 return time.Parse(time.RFC3339, version) 63 } 64 65 // releaseTimeToReleaseTag - converts a time to a string formatted as 66 // an official MinIO release tag. 67 // 68 // An official minio release tag looks like: 69 // `RELEASE.2017-09-29T19-16-56Z` 70 func releaseTimeToReleaseTag(releaseTime time.Time) string { 71 return "RELEASE." + releaseTime.Format(MinioReleaseTagTimeLayout) 72 } 73 74 // releaseTagToReleaseTime - reverse of `releaseTimeToReleaseTag()` 75 func releaseTagToReleaseTime(releaseTag string) (releaseTime time.Time, err error) { 76 fields := strings.Split(releaseTag, ".") 77 if len(fields) < 2 || len(fields) > 4 { 78 return releaseTime, fmt.Errorf("%s is not a valid release tag", releaseTag) 79 } 80 if fields[0] != "RELEASE" { 81 return releaseTime, fmt.Errorf("%s is not a valid release tag", releaseTag) 82 } 83 return time.Parse(MinioReleaseTagTimeLayout, fields[1]) 84 } 85 86 // getModTime - get the file modification time of `path` 87 func getModTime(path string) (t time.Time, err error) { 88 // Convert to absolute path 89 absPath, err := filepath.Abs(path) 90 if err != nil { 91 return t, fmt.Errorf("Unable to get absolute path of %s. %w", path, err) 92 } 93 94 // Version is minio non-standard, we will use minio binary's 95 // ModTime as release time. 96 fi, err := Stat(absPath) 97 if err != nil { 98 return t, fmt.Errorf("Unable to get ModTime of %s. %w", absPath, err) 99 } 100 101 // Return the ModTime 102 return fi.ModTime().UTC(), nil 103 } 104 105 // GetCurrentReleaseTime - returns this process's release time. If it 106 // is official minio version, parsed version is returned else minio 107 // binary's mod time is returned. 108 func GetCurrentReleaseTime() (releaseTime time.Time, err error) { 109 if releaseTime, err = minioVersionToReleaseTime(Version); err == nil { 110 return releaseTime, err 111 } 112 113 // Looks like version is minio non-standard, we use minio 114 // binary's ModTime as release time: 115 return getModTime(os.Args[0]) 116 } 117 118 // IsDocker - returns if the environment minio is running in docker or 119 // not. The check is a simple file existence check. 120 // 121 // https://github.com/moby/moby/blob/master/daemon/initlayer/setup_unix.go 122 // https://github.com/containers/podman/blob/master/libpod/runtime.go 123 // 124 // "/.dockerenv": "file", 125 // "/run/.containerenv": "file", 126 func IsDocker() bool { 127 var err error 128 for _, envfile := range []string{ 129 "/.dockerenv", 130 "/run/.containerenv", 131 } { 132 _, err = os.Stat(envfile) 133 if err == nil { 134 return true 135 } 136 } 137 if osIsNotExist(err) { 138 // if none of the files are present we may be running inside 139 // CRI-O, Containerd etc.. 140 // Fallback to our container specific ENVs if they are set. 141 return env.IsSet("MINIO_ACCESS_KEY_FILE") 142 } 143 144 // Log error, as we will not propagate it to caller 145 logger.LogIf(GlobalContext, err) 146 147 return err == nil 148 } 149 150 // IsDCOS returns true if minio is running in DCOS. 151 func IsDCOS() bool { 152 // http://mesos.apache.org/documentation/latest/docker-containerizer/ 153 // Mesos docker containerizer sets this value 154 return env.Get("MESOS_CONTAINER_NAME", "") != "" 155 } 156 157 // IsKubernetes returns true if minio is running in kubernetes. 158 func IsKubernetes() bool { 159 // Kubernetes env used to validate if we are 160 // indeed running inside a kubernetes pod 161 // is KUBERNETES_SERVICE_HOST 162 // https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/kubelet_pods.go#L541 163 return env.Get("KUBERNETES_SERVICE_HOST", "") != "" 164 } 165 166 // IsBOSH returns true if minio is deployed from a bosh package 167 func IsBOSH() bool { 168 // "/var/vcap/bosh" exists in BOSH deployed instance. 169 _, err := os.Stat("/var/vcap/bosh") 170 if osIsNotExist(err) { 171 return false 172 } 173 174 // Log error, as we will not propagate it to caller 175 logger.LogIf(GlobalContext, err) 176 177 return err == nil 178 } 179 180 // MinIO Helm chart uses DownwardAPIFile to write pod label info to /podinfo/labels 181 // More info: https://kubernetes.io/docs/tasks/inject-data-application/downward-api-volume-expose-pod-information/#store-pod-fields 182 // Check if this is Helm package installation and report helm chart version 183 func getHelmVersion(helmInfoFilePath string) string { 184 // Read the file exists. 185 helmInfoFile, err := Open(helmInfoFilePath) 186 if err != nil { 187 // Log errors and return "" as MinIO can be deployed 188 // without Helm charts as well. 189 if !osIsNotExist(err) { 190 reqInfo := (&logger.ReqInfo{}).AppendTags("helmInfoFilePath", helmInfoFilePath) 191 ctx := logger.SetReqInfo(GlobalContext, reqInfo) 192 logger.LogIf(ctx, err) 193 } 194 return "" 195 } 196 defer helmInfoFile.Close() 197 scanner := bufio.NewScanner(helmInfoFile) 198 for scanner.Scan() { 199 if strings.Contains(scanner.Text(), "chart=") { 200 helmChartVersion := strings.TrimPrefix(scanner.Text(), "chart=") 201 // remove quotes from the chart version 202 return strings.Trim(helmChartVersion, `"`) 203 } 204 } 205 206 return "" 207 } 208 209 // IsSourceBuild - returns if this binary is a non-official build from 210 // source code. 211 func IsSourceBuild() bool { 212 _, err := minioVersionToReleaseTime(Version) 213 return err != nil 214 } 215 216 // IsPCFTile returns if server is running in PCF 217 func IsPCFTile() bool { 218 return env.Get("MINIO_PCF_TILE_VERSION", "") != "" 219 } 220 221 // DO NOT CHANGE USER AGENT STYLE. 222 // The style should be 223 // 224 // MinIO (<OS>; <ARCH>[; <MODE>][; dcos][; kubernetes][; docker][; source]) MinIO/<VERSION> MinIO/<RELEASE-TAG> MinIO/<COMMIT-ID> [MinIO/universe-<PACKAGE-NAME>] [MinIO/helm-<HELM-VERSION>] 225 // 226 // Any change here should be discussed by opening an issue at 227 // https://github.com/minio/minio/issues. 228 func getUserAgent(mode string) string { 229 userAgentParts := []string{} 230 // Helper function to concisely append a pair of strings to a 231 // the user-agent slice. 232 uaAppend := func(p, q string) { 233 userAgentParts = append(userAgentParts, p, q) 234 } 235 uaAppend(MinioUAName, " (") 236 uaAppend("", runtime.GOOS) 237 uaAppend("; ", runtime.GOARCH) 238 if mode != "" { 239 uaAppend("; ", mode) 240 } 241 if IsDCOS() { 242 uaAppend("; ", "dcos") 243 } 244 if IsKubernetes() { 245 uaAppend("; ", "kubernetes") 246 } 247 if IsDocker() { 248 uaAppend("; ", "docker") 249 } 250 if IsBOSH() { 251 uaAppend("; ", "bosh") 252 } 253 if IsSourceBuild() { 254 uaAppend("; ", "source") 255 } 256 257 uaAppend(" ", Version) 258 uaAppend(" ", ReleaseTag) 259 uaAppend(" ", CommitID) 260 if IsDCOS() { 261 universePkgVersion := env.Get("MARATHON_APP_LABEL_DCOS_PACKAGE_VERSION", "") 262 // On DC/OS environment try to the get universe package version. 263 if universePkgVersion != "" { 264 uaAppend(" universe-", universePkgVersion) 265 } 266 } 267 268 if IsKubernetes() { 269 // In Kubernetes environment, try to fetch the helm package version 270 helmChartVersion := getHelmVersion("/podinfo/labels") 271 if helmChartVersion != "" { 272 uaAppend(" helm-", helmChartVersion) 273 } 274 // In Kubernetes environment, try to fetch the Operator, VSPHERE plugin version 275 opVersion := env.Get("MINIO_OPERATOR_VERSION", "") 276 if opVersion != "" { 277 uaAppend(" operator-", opVersion) 278 } 279 vsphereVersion := env.Get("MINIO_VSPHERE_PLUGIN_VERSION", "") 280 if vsphereVersion != "" { 281 uaAppend(" vsphere-plugin-", vsphereVersion) 282 } 283 } 284 285 if IsPCFTile() { 286 pcfTileVersion := env.Get("MINIO_PCF_TILE_VERSION", "") 287 if pcfTileVersion != "" { 288 uaAppend(" pcf-tile-", pcfTileVersion) 289 } 290 } 291 uaAppend("; ", "") 292 293 if cpus, err := gopsutilcpu.Info(); err == nil && len(cpus) > 0 { 294 cpuMap := make(map[string]struct{}, len(cpus)) 295 coreMap := make(map[string]struct{}, len(cpus)) 296 for i := range cpus { 297 cpuMap[cpus[i].PhysicalID] = struct{}{} 298 coreMap[cpus[i].CoreID] = struct{}{} 299 } 300 cpu := cpus[0] 301 uaAppend(" CPU ", fmt.Sprintf("(total_cpus:%d, total_cores:%d; vendor:%s; family:%s; model:%s; stepping:%d; model_name:%s)", 302 len(cpuMap), len(coreMap), cpu.VendorID, cpu.Family, cpu.Model, cpu.Stepping, cpu.ModelName)) 303 } 304 uaAppend(")", "") 305 306 return strings.Join(userAgentParts, "") 307 } 308 309 func downloadReleaseURL(u *url.URL, timeout time.Duration, mode string) (content string, err error) { 310 req, err := http.NewRequest(http.MethodGet, u.String(), nil) 311 if err != nil { 312 return content, AdminError{ 313 Code: AdminUpdateUnexpectedFailure, 314 Message: err.Error(), 315 StatusCode: http.StatusInternalServerError, 316 } 317 } 318 req.Header.Set("User-Agent", getUserAgent(mode)) 319 320 client := &http.Client{Transport: getUpdateTransport(timeout)} 321 resp, err := client.Do(req) 322 if err != nil { 323 if xnet.IsNetworkOrHostDown(err, false) { 324 return content, AdminError{ 325 Code: AdminUpdateURLNotReachable, 326 Message: err.Error(), 327 StatusCode: http.StatusServiceUnavailable, 328 } 329 } 330 return content, AdminError{ 331 Code: AdminUpdateUnexpectedFailure, 332 Message: err.Error(), 333 StatusCode: http.StatusInternalServerError, 334 } 335 } 336 if resp == nil { 337 return content, AdminError{ 338 Code: AdminUpdateUnexpectedFailure, 339 Message: fmt.Sprintf("No response from server to download URL %s", u), 340 StatusCode: http.StatusInternalServerError, 341 } 342 } 343 defer xhttp.DrainBody(resp.Body) 344 345 if resp.StatusCode != http.StatusOK { 346 return content, AdminError{ 347 Code: AdminUpdateUnexpectedFailure, 348 Message: fmt.Sprintf("Error downloading URL %s. Response: %v", u, resp.Status), 349 StatusCode: resp.StatusCode, 350 } 351 } 352 353 contentBytes, err := io.ReadAll(resp.Body) 354 if err != nil { 355 return content, AdminError{ 356 Code: AdminUpdateUnexpectedFailure, 357 Message: fmt.Sprintf("Error reading response. %s", err), 358 StatusCode: http.StatusInternalServerError, 359 } 360 } 361 362 return string(contentBytes), nil 363 } 364 365 func releaseInfoToReleaseTime(releaseInfo string) (releaseTime time.Time, err error) { 366 // Split release of style minio.RELEASE.2019-08-21T19-40-07Z.<hotfix> 367 nfields := strings.SplitN(releaseInfo, ".", 2) 368 if len(nfields) != 2 { 369 err = fmt.Errorf("Unknown release information `%s`", releaseInfo) 370 return releaseTime, err 371 } 372 if nfields[0] != "minio" { 373 err = fmt.Errorf("Unknown release `%s`", releaseInfo) 374 return releaseTime, err 375 } 376 377 releaseTime, err = releaseTagToReleaseTime(nfields[1]) 378 if err != nil { 379 err = fmt.Errorf("Unknown release tag format. %w", err) 380 } 381 return releaseTime, err 382 } 383 384 // parseReleaseData - parses release info file content fetched from 385 // official minio download server. 386 // 387 // The expected format is a single line with two words like: 388 // 389 // fbe246edbd382902db9a4035df7dce8cb441357d minio.RELEASE.2016-10-07T01-16-39Z.<hotfix_optional> 390 // 391 // The second word must be `minio.` appended to a standard release tag. 392 func parseReleaseData(data string) (sha256Sum []byte, releaseTime time.Time, releaseInfo string, err error) { 393 defer func() { 394 if err != nil { 395 err = AdminError{ 396 Code: AdminUpdateUnexpectedFailure, 397 Message: err.Error(), 398 StatusCode: http.StatusInternalServerError, 399 } 400 } 401 }() 402 403 fields := strings.Fields(data) 404 if len(fields) != 2 { 405 err = fmt.Errorf("Unknown release data `%s`", data) 406 return sha256Sum, releaseTime, releaseInfo, err 407 } 408 409 sha256Sum, err = hex.DecodeString(fields[0]) 410 if err != nil { 411 return sha256Sum, releaseTime, releaseInfo, err 412 } 413 414 releaseInfo = fields[1] 415 416 releaseTime, err = releaseInfoToReleaseTime(releaseInfo) 417 return sha256Sum, releaseTime, releaseInfo, err 418 } 419 420 func getUpdateTransport(timeout time.Duration) http.RoundTripper { 421 var updateTransport http.RoundTripper = &http.Transport{ 422 Proxy: http.ProxyFromEnvironment, 423 DialContext: xhttp.NewCustomDialContext(timeout, globalTCPOptions), 424 IdleConnTimeout: timeout, 425 TLSHandshakeTimeout: timeout, 426 ExpectContinueTimeout: timeout, 427 TLSClientConfig: &tls.Config{ 428 RootCAs: globalRootCAs, 429 ClientSessionCache: tls.NewLRUClientSessionCache(tlsClientSessionCacheSize), 430 }, 431 DisableCompression: true, 432 } 433 return updateTransport 434 } 435 436 func getLatestReleaseTime(u *url.URL, timeout time.Duration, mode string) (sha256Sum []byte, releaseTime time.Time, err error) { 437 data, err := downloadReleaseURL(u, timeout, mode) 438 if err != nil { 439 return sha256Sum, releaseTime, err 440 } 441 442 sha256Sum, releaseTime, _, err = parseReleaseData(data) 443 return 444 } 445 446 const ( 447 // Kubernetes deployment doc link. 448 kubernetesDeploymentDoc = "https://min.io/docs/minio/kubernetes/upstream/index.html#quickstart-for-kubernetes" 449 450 // Mesos deployment doc link. 451 mesosDeploymentDoc = "https://min.io/docs/minio/kubernetes/upstream/index.html#quickstart-for-kubernetes" 452 ) 453 454 func getDownloadURL(releaseTag string) (downloadURL string) { 455 // Check if we are in DCOS environment, return 456 // deployment guide for update procedures. 457 if IsDCOS() { 458 return mesosDeploymentDoc 459 } 460 461 // Check if we are in kubernetes environment, return 462 // deployment guide for update procedures. 463 if IsKubernetes() { 464 return kubernetesDeploymentDoc 465 } 466 467 // Check if we are docker environment, return docker update command 468 if IsDocker() { 469 // Construct release tag name. 470 return fmt.Sprintf("podman pull quay.io/minio/minio:%s", releaseTag) 471 } 472 473 // For binary only installations, we return link to the latest binary. 474 if runtime.GOOS == "windows" { 475 return MinioReleaseURL + "minio.exe" 476 } 477 478 return MinioReleaseURL + "minio" 479 } 480 481 func getUpdateReaderFromURL(u *url.URL, transport http.RoundTripper, mode string) (io.ReadCloser, error) { 482 clnt := &http.Client{ 483 Transport: transport, 484 } 485 req, err := http.NewRequest(http.MethodGet, u.String(), nil) 486 if err != nil { 487 return nil, AdminError{ 488 Code: AdminUpdateUnexpectedFailure, 489 Message: err.Error(), 490 StatusCode: http.StatusInternalServerError, 491 } 492 } 493 494 req.Header.Set("User-Agent", getUserAgent(mode)) 495 496 resp, err := clnt.Do(req) 497 if err != nil { 498 if xnet.IsNetworkOrHostDown(err, false) { 499 return nil, AdminError{ 500 Code: AdminUpdateURLNotReachable, 501 Message: err.Error(), 502 StatusCode: http.StatusServiceUnavailable, 503 } 504 } 505 return nil, AdminError{ 506 Code: AdminUpdateUnexpectedFailure, 507 Message: err.Error(), 508 StatusCode: http.StatusInternalServerError, 509 } 510 } 511 return resp.Body, nil 512 } 513 514 var updateInProgress atomic.Uint32 515 516 // Function to get the reader from an architecture 517 func downloadBinary(u *url.URL, mode string) (binCompressed []byte, bin []byte, err error) { 518 transport := getUpdateTransport(30 * time.Second) 519 var reader io.ReadCloser 520 if u.Scheme == "https" || u.Scheme == "http" { 521 reader, err = getUpdateReaderFromURL(u, transport, mode) 522 if err != nil { 523 return nil, nil, err 524 } 525 } else { 526 return nil, nil, fmt.Errorf("unsupported protocol scheme: %s", u.Scheme) 527 } 528 defer xhttp.DrainBody(reader) 529 530 b := bytebufferpool.Get() 531 bc := bytebufferpool.Get() 532 defer func() { 533 b.Reset() 534 bc.Reset() 535 536 bytebufferpool.Put(b) 537 bytebufferpool.Put(bc) 538 }() 539 540 w, err := zstd.NewWriter(bc) 541 if err != nil { 542 return nil, nil, err 543 } 544 545 if _, err = io.Copy(w, io.TeeReader(reader, b)); err != nil { 546 return nil, nil, err 547 } 548 549 w.Close() 550 return bc.Bytes(), b.Bytes(), nil 551 } 552 553 const ( 554 // Update this whenever the official minisign pubkey is rotated. 555 defaultMinisignPubkey = "RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGav" 556 ) 557 558 func verifyBinary(u *url.URL, sha256Sum []byte, releaseInfo, mode string, reader io.Reader) (err error) { 559 if !updateInProgress.CompareAndSwap(0, 1) { 560 return errors.New("update already in progress") 561 } 562 defer updateInProgress.Store(0) 563 564 transport := getUpdateTransport(30 * time.Second) 565 opts := selfupdate.Options{ 566 Hash: crypto.SHA256, 567 Checksum: sha256Sum, 568 } 569 570 if err := opts.CheckPermissions(); err != nil { 571 return AdminError{ 572 Code: AdminUpdateApplyFailure, 573 Message: fmt.Sprintf("server update failed with: %s, do not restart the servers yet", err), 574 StatusCode: http.StatusInternalServerError, 575 } 576 } 577 578 minisignPubkey := env.Get(envMinisignPubKey, defaultMinisignPubkey) 579 if minisignPubkey != "" { 580 v := selfupdate.NewVerifier() 581 u.Path = path.Dir(u.Path) + slashSeparator + releaseInfo + ".minisig" 582 if err = v.LoadFromURL(u.String(), minisignPubkey, transport); err != nil { 583 return AdminError{ 584 Code: AdminUpdateApplyFailure, 585 Message: fmt.Sprintf("signature loading failed for %v with %v", u, err), 586 StatusCode: http.StatusInternalServerError, 587 } 588 } 589 opts.Verifier = v 590 } 591 592 if err = selfupdate.PrepareAndCheckBinary(reader, opts); err != nil { 593 var pathErr *os.PathError 594 if errors.As(err, &pathErr) { 595 return AdminError{ 596 Code: AdminUpdateApplyFailure, 597 Message: fmt.Sprintf("Unable to update the binary at %s: %v", 598 filepath.Dir(pathErr.Path), pathErr.Err), 599 StatusCode: http.StatusForbidden, 600 } 601 } 602 return AdminError{ 603 Code: AdminUpdateApplyFailure, 604 Message: err.Error(), 605 StatusCode: http.StatusInternalServerError, 606 } 607 } 608 609 return nil 610 } 611 612 func commitBinary() (err error) { 613 if !updateInProgress.CompareAndSwap(0, 1) { 614 return errors.New("update already in progress") 615 } 616 defer updateInProgress.Store(0) 617 618 opts := selfupdate.Options{} 619 620 if err = selfupdate.CommitBinary(opts); err != nil { 621 if rerr := selfupdate.RollbackError(err); rerr != nil { 622 return AdminError{ 623 Code: AdminUpdateApplyFailure, 624 Message: fmt.Sprintf("Failed to rollback from bad update: %v", rerr), 625 StatusCode: http.StatusInternalServerError, 626 } 627 } 628 var pathErr *os.PathError 629 if errors.As(err, &pathErr) { 630 return AdminError{ 631 Code: AdminUpdateApplyFailure, 632 Message: fmt.Sprintf("Unable to update the binary at %s: %v", 633 filepath.Dir(pathErr.Path), pathErr.Err), 634 StatusCode: http.StatusForbidden, 635 } 636 } 637 return AdminError{ 638 Code: AdminUpdateApplyFailure, 639 Message: err.Error(), 640 StatusCode: http.StatusInternalServerError, 641 } 642 } 643 644 return nil 645 }