storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/cmd/update.go (about)

     1  /*
     2   * MinIO Cloud Storage, (C) 2015-2021 MinIO, Inc.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package cmd
    18  
    19  import (
    20  	"bufio"
    21  	"crypto"
    22  	"crypto/tls"
    23  	"encoding/hex"
    24  	"errors"
    25  	"fmt"
    26  	"io"
    27  	"io/ioutil"
    28  	"net/http"
    29  	"net/url"
    30  	"os"
    31  	"path"
    32  	"path/filepath"
    33  	"runtime"
    34  	"strings"
    35  	"time"
    36  
    37  	"github.com/minio/selfupdate"
    38  
    39  	xhttp "storj.io/minio/cmd/http"
    40  	"storj.io/minio/cmd/logger"
    41  	"storj.io/minio/pkg/env"
    42  	xnet "storj.io/minio/pkg/net"
    43  )
    44  
    45  const (
    46  	minioReleaseTagTimeLayout = "2006-01-02T15-04-05Z"
    47  	minioOSARCH               = runtime.GOOS + "-" + runtime.GOARCH
    48  	minioReleaseURL           = "https://dl.min.io/server/minio/release/" + minioOSARCH + SlashSeparator
    49  
    50  	envMinisignPubKey = "MINIO_UPDATE_MINISIGN_PUBKEY"
    51  	updateTimeout     = 10 * time.Second
    52  )
    53  
    54  var (
    55  	// For windows our files have .exe additionally.
    56  	minioReleaseWindowsInfoURL = minioReleaseURL + "minio.exe.sha256sum"
    57  )
    58  
    59  // minioVersionToReleaseTime - parses a standard official release
    60  // MinIO version string.
    61  //
    62  // An official binary's version string is the release time formatted
    63  // with RFC3339 (in UTC) - e.g. `2017-09-29T19:16:56Z`
    64  func minioVersionToReleaseTime(version string) (releaseTime time.Time, err error) {
    65  	return time.Parse(time.RFC3339, version)
    66  }
    67  
    68  // releaseTimeToReleaseTag - converts a time to a string formatted as
    69  // an official MinIO release tag.
    70  //
    71  // An official minio release tag looks like:
    72  // `RELEASE.2017-09-29T19-16-56Z`
    73  func releaseTimeToReleaseTag(releaseTime time.Time) string {
    74  	return "RELEASE." + releaseTime.Format(minioReleaseTagTimeLayout)
    75  }
    76  
    77  // releaseTagToReleaseTime - reverse of `releaseTimeToReleaseTag()`
    78  func releaseTagToReleaseTime(releaseTag string) (releaseTime time.Time, err error) {
    79  	fields := strings.Split(releaseTag, ".")
    80  	if len(fields) < 2 || len(fields) > 3 {
    81  		return releaseTime, fmt.Errorf("%s is not a valid release tag", releaseTag)
    82  	}
    83  	if fields[0] != "RELEASE" {
    84  		return releaseTime, fmt.Errorf("%s is not a valid release tag", releaseTag)
    85  	}
    86  	return time.Parse(minioReleaseTagTimeLayout, fields[1])
    87  }
    88  
    89  // getModTime - get the file modification time of `path`
    90  func getModTime(path string) (t time.Time, err error) {
    91  	// Convert to absolute path
    92  	absPath, err := filepath.Abs(path)
    93  	if err != nil {
    94  		return t, fmt.Errorf("Unable to get absolute path of %s. %w", path, err)
    95  	}
    96  
    97  	// Version is minio non-standard, we will use minio binary's
    98  	// ModTime as release time.
    99  	fi, err := os.Stat(absPath)
   100  	if err != nil {
   101  		return t, fmt.Errorf("Unable to get ModTime of %s. %w", absPath, err)
   102  	}
   103  
   104  	// Return the ModTime
   105  	return fi.ModTime().UTC(), nil
   106  }
   107  
   108  // GetCurrentReleaseTime - returns this process's release time.  If it
   109  // is official minio version, parsed version is returned else minio
   110  // binary's mod time is returned.
   111  func GetCurrentReleaseTime() (releaseTime time.Time, err error) {
   112  	if releaseTime, err = minioVersionToReleaseTime(Version); err == nil {
   113  		return releaseTime, err
   114  	}
   115  
   116  	// Looks like version is minio non-standard, we use minio
   117  	// binary's ModTime as release time:
   118  	return getModTime(os.Args[0])
   119  }
   120  
   121  // IsDocker - returns if the environment minio is running in docker or
   122  // not. The check is a simple file existence check.
   123  //
   124  // https://github.com/moby/moby/blob/master/daemon/initlayer/setup_unix.go#L25
   125  //
   126  //     "/.dockerenv":      "file",
   127  //
   128  func IsDocker() bool {
   129  	if env.Get("MINIO_CI_CD", "") == "" {
   130  		_, err := os.Stat("/.dockerenv")
   131  		if osIsNotExist(err) {
   132  			return false
   133  		}
   134  
   135  		// Log error, as we will not propagate it to caller
   136  		logger.LogIf(GlobalContext, err)
   137  
   138  		return err == nil
   139  	}
   140  	return false
   141  }
   142  
   143  // IsDCOS returns true if minio is running in DCOS.
   144  func IsDCOS() bool {
   145  	if env.Get("MINIO_CI_CD", "") == "" {
   146  		// http://mesos.apache.org/documentation/latest/docker-containerizer/
   147  		// Mesos docker containerizer sets this value
   148  		return env.Get("MESOS_CONTAINER_NAME", "") != ""
   149  	}
   150  	return false
   151  }
   152  
   153  // IsKubernetesReplicaSet returns true if minio is running in kubernetes replica set.
   154  func IsKubernetesReplicaSet() bool {
   155  	return IsKubernetes() && (env.Get("KUBERNETES_REPLICA_SET", "") != "")
   156  }
   157  
   158  // IsKubernetes returns true if minio is running in kubernetes.
   159  func IsKubernetes() bool {
   160  	if env.Get("MINIO_CI_CD", "") == "" {
   161  		// Kubernetes env used to validate if we are
   162  		// indeed running inside a kubernetes pod
   163  		// is KUBERNETES_SERVICE_HOST
   164  		// https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/kubelet_pods.go#L541
   165  		return env.Get("KUBERNETES_SERVICE_HOST", "") != ""
   166  	}
   167  	return false
   168  }
   169  
   170  // IsBOSH returns true if minio is deployed from a bosh package
   171  func IsBOSH() bool {
   172  	// "/var/vcap/bosh" exists in BOSH deployed instance.
   173  	_, err := os.Stat("/var/vcap/bosh")
   174  	if osIsNotExist(err) {
   175  		return false
   176  	}
   177  
   178  	// Log error, as we will not propagate it to caller
   179  	logger.LogIf(GlobalContext, err)
   180  
   181  	return err == nil
   182  }
   183  
   184  // MinIO Helm chart uses DownwardAPIFile to write pod label info to /podinfo/labels
   185  // More info: https://kubernetes.io/docs/tasks/inject-data-application/downward-api-volume-expose-pod-information/#store-pod-fields
   186  // Check if this is Helm package installation and report helm chart version
   187  func getHelmVersion(helmInfoFilePath string) string {
   188  	// Read the file exists.
   189  	helmInfoFile, err := os.Open(helmInfoFilePath)
   190  	if err != nil {
   191  		// Log errors and return "" as MinIO can be deployed
   192  		// without Helm charts as well.
   193  		if !osIsNotExist(err) {
   194  			reqInfo := (&logger.ReqInfo{}).AppendTags("helmInfoFilePath", helmInfoFilePath)
   195  			ctx := logger.SetReqInfo(GlobalContext, reqInfo)
   196  			logger.LogIf(ctx, err)
   197  		}
   198  		return ""
   199  	}
   200  
   201  	scanner := bufio.NewScanner(helmInfoFile)
   202  	for scanner.Scan() {
   203  		if strings.Contains(scanner.Text(), "chart=") {
   204  			helmChartVersion := strings.TrimPrefix(scanner.Text(), "chart=")
   205  			// remove quotes from the chart version
   206  			return strings.Trim(helmChartVersion, `"`)
   207  		}
   208  	}
   209  
   210  	return ""
   211  }
   212  
   213  // IsSourceBuild - returns if this binary is a non-official build from
   214  // source code.
   215  func IsSourceBuild() bool {
   216  	_, err := minioVersionToReleaseTime(Version)
   217  	return err != nil
   218  }
   219  
   220  // IsPCFTile returns if server is running in PCF
   221  func IsPCFTile() bool {
   222  	return env.Get("MINIO_PCF_TILE_VERSION", "") != ""
   223  }
   224  
   225  // DO NOT CHANGE USER AGENT STYLE.
   226  // The style should be
   227  //
   228  //   MinIO (<OS>; <ARCH>[; <MODE>][; dcos][; kubernetes][; docker][; source]) MinIO/<VERSION> MinIO/<RELEASE-TAG> MinIO/<COMMIT-ID> [MinIO/universe-<PACKAGE-NAME>] [MinIO/helm-<HELM-VERSION>]
   229  //
   230  // Any change here should be discussed by opening an issue at
   231  // https://github.com/minio/minio/issues.
   232  func getUserAgent(mode string) string {
   233  
   234  	userAgentParts := []string{}
   235  	// Helper function to concisely append a pair of strings to a
   236  	// the user-agent slice.
   237  	uaAppend := func(p, q string) {
   238  		userAgentParts = append(userAgentParts, p, q)
   239  	}
   240  
   241  	uaAppend("MinIO (", runtime.GOOS)
   242  	uaAppend("; ", runtime.GOARCH)
   243  	if mode != "" {
   244  		uaAppend("; ", mode)
   245  	}
   246  	if IsDCOS() {
   247  		uaAppend("; ", "dcos")
   248  	}
   249  	if IsKubernetes() {
   250  		uaAppend("; ", "kubernetes")
   251  	}
   252  	if IsDocker() {
   253  		uaAppend("; ", "docker")
   254  	}
   255  	if IsBOSH() {
   256  		uaAppend("; ", "bosh")
   257  	}
   258  	if IsSourceBuild() {
   259  		uaAppend("; ", "source")
   260  	}
   261  
   262  	uaAppend(") MinIO/", Version)
   263  	uaAppend(" MinIO/", ReleaseTag)
   264  	uaAppend(" MinIO/", CommitID)
   265  	if IsDCOS() {
   266  		universePkgVersion := env.Get("MARATHON_APP_LABEL_DCOS_PACKAGE_VERSION", "")
   267  		// On DC/OS environment try to the get universe package version.
   268  		if universePkgVersion != "" {
   269  			uaAppend(" MinIO/universe-", universePkgVersion)
   270  		}
   271  	}
   272  
   273  	if IsKubernetes() {
   274  		// In Kubernetes environment, try to fetch the helm package version
   275  		helmChartVersion := getHelmVersion("/podinfo/labels")
   276  		if helmChartVersion != "" {
   277  			uaAppend(" MinIO/helm-", helmChartVersion)
   278  		}
   279  		// In Kubernetes environment, try to fetch the Operator, VSPHERE plugin version
   280  		opVersion := env.Get("MINIO_OPERATOR_VERSION", "")
   281  		if opVersion != "" {
   282  			uaAppend(" MinIO/operator-", opVersion)
   283  		}
   284  		vsphereVersion := env.Get("MINIO_VSPHERE_PLUGIN_VERSION", "")
   285  		if vsphereVersion != "" {
   286  			uaAppend(" MinIO/vsphere-plugin-", vsphereVersion)
   287  		}
   288  	}
   289  
   290  	if IsPCFTile() {
   291  		pcfTileVersion := env.Get("MINIO_PCF_TILE_VERSION", "")
   292  		if pcfTileVersion != "" {
   293  			uaAppend(" MinIO/pcf-tile-", pcfTileVersion)
   294  		}
   295  	}
   296  
   297  	return strings.Join(userAgentParts, "")
   298  }
   299  
   300  func downloadReleaseURL(u *url.URL, timeout time.Duration, mode string) (content string, err error) {
   301  	var reader io.ReadCloser
   302  	if u.Scheme == "https" || u.Scheme == "http" {
   303  		req, err := http.NewRequest(http.MethodGet, u.String(), nil)
   304  		if err != nil {
   305  			return content, AdminError{
   306  				Code:       AdminUpdateUnexpectedFailure,
   307  				Message:    err.Error(),
   308  				StatusCode: http.StatusInternalServerError,
   309  			}
   310  		}
   311  		req.Header.Set("User-Agent", getUserAgent(mode))
   312  
   313  		client := &http.Client{Transport: getUpdateTransport(timeout)}
   314  		resp, err := client.Do(req)
   315  		if err != nil {
   316  			if xnet.IsNetworkOrHostDown(err, false) {
   317  				return content, AdminError{
   318  					Code:       AdminUpdateURLNotReachable,
   319  					Message:    err.Error(),
   320  					StatusCode: http.StatusServiceUnavailable,
   321  				}
   322  			}
   323  			return content, AdminError{
   324  				Code:       AdminUpdateUnexpectedFailure,
   325  				Message:    err.Error(),
   326  				StatusCode: http.StatusInternalServerError,
   327  			}
   328  		}
   329  		if resp == nil {
   330  			return content, AdminError{
   331  				Code:       AdminUpdateUnexpectedFailure,
   332  				Message:    fmt.Sprintf("No response from server to download URL %s", u),
   333  				StatusCode: http.StatusInternalServerError,
   334  			}
   335  		}
   336  		reader = resp.Body
   337  		defer xhttp.DrainBody(resp.Body)
   338  
   339  		if resp.StatusCode != http.StatusOK {
   340  			return content, AdminError{
   341  				Code:       AdminUpdateUnexpectedFailure,
   342  				Message:    fmt.Sprintf("Error downloading URL %s. Response: %v", u, resp.Status),
   343  				StatusCode: resp.StatusCode,
   344  			}
   345  		}
   346  	} else {
   347  		reader, err = os.Open(u.Path)
   348  		if err != nil {
   349  			return content, AdminError{
   350  				Code:       AdminUpdateURLNotReachable,
   351  				Message:    err.Error(),
   352  				StatusCode: http.StatusServiceUnavailable,
   353  			}
   354  		}
   355  	}
   356  
   357  	contentBytes, err := ioutil.ReadAll(reader)
   358  	if err != nil {
   359  		return content, AdminError{
   360  			Code:       AdminUpdateUnexpectedFailure,
   361  			Message:    fmt.Sprintf("Error reading response. %s", err),
   362  			StatusCode: http.StatusInternalServerError,
   363  		}
   364  	}
   365  
   366  	return string(contentBytes), nil
   367  }
   368  
   369  // parseReleaseData - parses release info file content fetched from
   370  // official minio download server.
   371  //
   372  // The expected format is a single line with two words like:
   373  //
   374  // fbe246edbd382902db9a4035df7dce8cb441357d minio.RELEASE.2016-10-07T01-16-39Z.<hotfix_optional>
   375  //
   376  // The second word must be `minio.` appended to a standard release tag.
   377  func parseReleaseData(data string) (sha256Sum []byte, releaseTime time.Time, releaseInfo string, err error) {
   378  	defer func() {
   379  		if err != nil {
   380  			err = AdminError{
   381  				Code:       AdminUpdateUnexpectedFailure,
   382  				Message:    err.Error(),
   383  				StatusCode: http.StatusInternalServerError,
   384  			}
   385  		}
   386  	}()
   387  
   388  	fields := strings.Fields(data)
   389  	if len(fields) != 2 {
   390  		err = fmt.Errorf("Unknown release data `%s`", data)
   391  		return sha256Sum, releaseTime, releaseInfo, err
   392  	}
   393  
   394  	sha256Sum, err = hex.DecodeString(fields[0])
   395  	if err != nil {
   396  		return sha256Sum, releaseTime, releaseInfo, err
   397  	}
   398  
   399  	releaseInfo = fields[1]
   400  
   401  	// Split release of style minio.RELEASE.2019-08-21T19-40-07Z.<hotfix>
   402  	nfields := strings.SplitN(releaseInfo, ".", 2)
   403  	if len(nfields) != 2 {
   404  		err = fmt.Errorf("Unknown release information `%s`", releaseInfo)
   405  		return sha256Sum, releaseTime, releaseInfo, err
   406  	}
   407  	if nfields[0] != "minio" {
   408  		err = fmt.Errorf("Unknown release `%s`", releaseInfo)
   409  		return sha256Sum, releaseTime, releaseInfo, err
   410  	}
   411  
   412  	releaseTime, err = releaseTagToReleaseTime(nfields[1])
   413  	if err != nil {
   414  		err = fmt.Errorf("Unknown release tag format. %w", err)
   415  	}
   416  
   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),
   424  		IdleConnTimeout:       timeout,
   425  		TLSHandshakeTimeout:   timeout,
   426  		ExpectContinueTimeout: timeout,
   427  		TLSClientConfig: &tls.Config{
   428  			RootCAs: globalRootCAs,
   429  		},
   430  		DisableCompression: true,
   431  	}
   432  	return updateTransport
   433  }
   434  
   435  func getLatestReleaseTime(u *url.URL, timeout time.Duration, mode string) (sha256Sum []byte, releaseTime time.Time, err error) {
   436  	data, err := downloadReleaseURL(u, timeout, mode)
   437  	if err != nil {
   438  		return sha256Sum, releaseTime, err
   439  	}
   440  
   441  	sha256Sum, releaseTime, _, err = parseReleaseData(data)
   442  	return
   443  }
   444  
   445  const (
   446  	// Kubernetes deployment doc link.
   447  	kubernetesDeploymentDoc = "https://docs.min.io/docs/deploy-minio-on-kubernetes"
   448  
   449  	// Mesos deployment doc link.
   450  	mesosDeploymentDoc = "https://docs.min.io/docs/deploy-minio-on-dc-os"
   451  )
   452  
   453  func getDownloadURL(releaseTag string) (downloadURL string) {
   454  	// Check if we are in DCOS environment, return
   455  	// deployment guide for update procedures.
   456  	if IsDCOS() {
   457  		return mesosDeploymentDoc
   458  	}
   459  
   460  	// Check if we are in kubernetes environment, return
   461  	// deployment guide for update procedures.
   462  	if IsKubernetes() {
   463  		return kubernetesDeploymentDoc
   464  	}
   465  
   466  	// Check if we are docker environment, return docker update command
   467  	if IsDocker() {
   468  		// Construct release tag name.
   469  		return fmt.Sprintf("docker pull minio/minio:%s", releaseTag)
   470  	}
   471  
   472  	// For binary only installations, we return link to the latest binary.
   473  	if runtime.GOOS == "windows" {
   474  		return minioReleaseURL + "minio.exe"
   475  	}
   476  
   477  	return minioReleaseURL + "minio"
   478  }
   479  
   480  func getUpdateReaderFromFile(u *url.URL) (io.ReadCloser, error) {
   481  	r, err := os.Open(u.Path)
   482  	if err != nil {
   483  		return nil, AdminError{
   484  			Code:       AdminUpdateUnexpectedFailure,
   485  			Message:    err.Error(),
   486  			StatusCode: http.StatusInternalServerError,
   487  		}
   488  	}
   489  	return r, nil
   490  }
   491  
   492  func getUpdateReaderFromURL(u *url.URL, transport http.RoundTripper, mode string) (io.ReadCloser, error) {
   493  	clnt := &http.Client{
   494  		Transport: transport,
   495  	}
   496  	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
   497  	if err != nil {
   498  		return nil, AdminError{
   499  			Code:       AdminUpdateUnexpectedFailure,
   500  			Message:    err.Error(),
   501  			StatusCode: http.StatusInternalServerError,
   502  		}
   503  	}
   504  
   505  	req.Header.Set("User-Agent", getUserAgent(mode))
   506  
   507  	resp, err := clnt.Do(req)
   508  	if err != nil {
   509  		if xnet.IsNetworkOrHostDown(err, false) {
   510  			return nil, AdminError{
   511  				Code:       AdminUpdateURLNotReachable,
   512  				Message:    err.Error(),
   513  				StatusCode: http.StatusServiceUnavailable,
   514  			}
   515  		}
   516  		return nil, AdminError{
   517  			Code:       AdminUpdateUnexpectedFailure,
   518  			Message:    err.Error(),
   519  			StatusCode: http.StatusInternalServerError,
   520  		}
   521  	}
   522  	return resp.Body, nil
   523  }
   524  
   525  func doUpdate(u *url.URL, lrTime time.Time, sha256Sum []byte, releaseInfo string, mode string) (err error) {
   526  	transport := getUpdateTransport(30 * time.Second)
   527  	var reader io.ReadCloser
   528  	if u.Scheme == "https" || u.Scheme == "http" {
   529  		reader, err = getUpdateReaderFromURL(u, transport, mode)
   530  		if err != nil {
   531  			return err
   532  		}
   533  	} else {
   534  		reader, err = getUpdateReaderFromFile(u)
   535  		if err != nil {
   536  			return err
   537  		}
   538  	}
   539  
   540  	opts := selfupdate.Options{
   541  		Hash:     crypto.SHA256,
   542  		Checksum: sha256Sum,
   543  	}
   544  
   545  	minisignPubkey := env.Get(envMinisignPubKey, "")
   546  	if minisignPubkey != "" {
   547  		v := selfupdate.NewVerifier()
   548  		u.Path = path.Dir(u.Path) + slashSeparator + releaseInfo + ".minisig"
   549  		if err = v.LoadFromURL(u.String(), minisignPubkey, transport); err != nil {
   550  			return AdminError{
   551  				Code:       AdminUpdateApplyFailure,
   552  				Message:    fmt.Sprintf("signature loading failed for %v with %v", u, err),
   553  				StatusCode: http.StatusInternalServerError,
   554  			}
   555  		}
   556  		opts.Verifier = v
   557  	}
   558  
   559  	if err = selfupdate.Apply(reader, opts); err != nil {
   560  		if rerr := selfupdate.RollbackError(err); rerr != nil {
   561  			return AdminError{
   562  				Code:       AdminUpdateApplyFailure,
   563  				Message:    fmt.Sprintf("Failed to rollback from bad update: %v", rerr),
   564  				StatusCode: http.StatusInternalServerError,
   565  			}
   566  		}
   567  		var pathErr *os.PathError
   568  		if errors.As(err, &pathErr) {
   569  			return AdminError{
   570  				Code: AdminUpdateApplyFailure,
   571  				Message: fmt.Sprintf("Unable to update the binary at %s: %v",
   572  					filepath.Dir(pathErr.Path), pathErr.Err),
   573  				StatusCode: http.StatusForbidden,
   574  			}
   575  		}
   576  		return AdminError{
   577  			Code:       AdminUpdateApplyFailure,
   578  			Message:    err.Error(),
   579  			StatusCode: http.StatusInternalServerError,
   580  		}
   581  	}
   582  
   583  	return nil
   584  }