github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/subnet-utils.go (about)

     1  // Copyright (c) 2015-2024 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  	"bytes"
    23  	"encoding/base64"
    24  	"encoding/json"
    25  	"errors"
    26  	"fmt"
    27  	"io"
    28  	"math"
    29  	"net/http"
    30  	"net/http/httputil"
    31  	"net/url"
    32  	"os"
    33  	"strings"
    34  	"time"
    35  
    36  	"github.com/google/uuid"
    37  	"github.com/minio/cli"
    38  	"github.com/minio/madmin-go/v3"
    39  	"github.com/minio/mc/pkg/probe"
    40  	"github.com/minio/pkg/v2/licverifier"
    41  	"github.com/minio/pkg/v2/subnet"
    42  	"github.com/tidwall/gjson"
    43  	"golang.org/x/term"
    44  )
    45  
    46  const (
    47  	subnetRespBodyLimit     = 1 << 20 // 1 MiB
    48  	minioSubscriptionURL    = "https://min.io/subscription"
    49  	subnetPublicKeyPath     = "/downloads/license-pubkey.pem"
    50  	minioDeploymentIDHeader = "x-minio-deployment-id"
    51  )
    52  
    53  var subnetCommonFlags = append(supportGlobalFlags, cli.StringFlag{
    54  	Name:   "api-key",
    55  	Usage:  "API Key of the account on SUBNET",
    56  	EnvVar: "_MC_SUBNET_API_KEY",
    57  })
    58  
    59  // SubnetBaseURL - returns the base URL of SUBNET
    60  func SubnetBaseURL() string {
    61  	return subnet.BaseURL(GlobalDevMode)
    62  }
    63  
    64  func subnetIssueURL(issueNum int) string {
    65  	return fmt.Sprintf("%s/issues/%d", SubnetBaseURL(), issueNum)
    66  }
    67  
    68  func subnetLogWebhookURL() string {
    69  	return SubnetBaseURL() + "/api/logs"
    70  }
    71  
    72  // SubnetUploadURL - returns the upload URL for the given upload type
    73  func SubnetUploadURL(uploadType string) string {
    74  	return fmt.Sprintf("%s/api/%s/upload", SubnetBaseURL(), uploadType)
    75  }
    76  
    77  // SubnetRegisterURL - returns the cluster registration URL
    78  func SubnetRegisterURL() string {
    79  	return SubnetBaseURL() + "/api/cluster/register"
    80  }
    81  
    82  func subnetUnregisterURL(depID string) string {
    83  	return SubnetBaseURL() + "/api/cluster/unregister?deploymentId=" + depID
    84  }
    85  
    86  func subnetLicenseRenewURL() string {
    87  	return SubnetBaseURL() + "/api/cluster/renew-license"
    88  }
    89  
    90  func subnetOfflineRegisterURL(regToken string) string {
    91  	return SubnetBaseURL() + "/cluster/register?token=" + regToken
    92  }
    93  
    94  func subnetLoginURL() string {
    95  	return SubnetBaseURL() + "/api/auth/login"
    96  }
    97  
    98  func subnetAPIKeyURL() string {
    99  	return SubnetBaseURL() + "/api/auth/api-key"
   100  }
   101  
   102  func subnetMFAURL() string {
   103  	return SubnetBaseURL() + "/api/auth/mfa-login"
   104  }
   105  
   106  func checkURLReachable(url string) *probe.Error {
   107  	_, e := subnetHeadReq(url, nil)
   108  	if e != nil {
   109  		return probe.NewError(e).Trace(url)
   110  	}
   111  	return nil
   112  }
   113  
   114  func subnetURLWithAuth(reqURL, apiKey string) (string, map[string]string, error) {
   115  	if len(apiKey) == 0 {
   116  		// API key not available in minio/mc config.
   117  		// Ask the user to log in to get auth token
   118  		token, e := subnetLogin()
   119  		if e != nil {
   120  			return "", nil, e
   121  		}
   122  		apiKey, e = getSubnetAPIKeyUsingAuthToken(token)
   123  		if e != nil {
   124  			return "", nil, e
   125  		}
   126  	}
   127  	return reqURL, SubnetAPIKeyAuthHeaders(apiKey), nil
   128  }
   129  
   130  // SubnetHeaders - type for SUBNET request headers
   131  type SubnetHeaders map[string]string
   132  
   133  func (h SubnetHeaders) addDeploymentIDHeader(alias string) {
   134  	h[minioDeploymentIDHeader] = getAdminInfo(alias).DeploymentID
   135  }
   136  
   137  func subnetTokenAuthHeaders(authToken string) map[string]string {
   138  	return map[string]string{"Authorization": "Bearer " + authToken}
   139  }
   140  
   141  // SubnetLicenseAuthHeaders - returns the headers for SUBNET license authentication
   142  func SubnetLicenseAuthHeaders(lic string) map[string]string {
   143  	return map[string]string{"x-subnet-license": lic}
   144  }
   145  
   146  // SubnetAPIKeyAuthHeaders - returns the headers for SUBNET API key authentication
   147  func SubnetAPIKeyAuthHeaders(apiKey string) SubnetHeaders {
   148  	return map[string]string{"x-subnet-api-key": apiKey}
   149  }
   150  
   151  func getSubnetClient() *http.Client {
   152  	client := httpClient(0)
   153  	if GlobalSubnetProxyURL != nil {
   154  		client.Transport.(*http.Transport).Proxy = http.ProxyURL(GlobalSubnetProxyURL)
   155  	}
   156  	return client
   157  }
   158  
   159  func subnetHTTPDo(req *http.Request) (resp *http.Response, err error) {
   160  	resp, err = getSubnetClient().Do(req)
   161  	if err == nil && globalDebug {
   162  		dumpHTTPReq(req, resp)
   163  	}
   164  	return
   165  }
   166  
   167  // dumpHTTP - dump HTTP request and response.
   168  func dumpHTTPReq(req *http.Request, resp *http.Response) error {
   169  	// Starts http dump.
   170  	_, err := fmt.Fprintln(os.Stderr, "---------START-HTTP---------")
   171  	if err != nil {
   172  		return err
   173  	}
   174  
   175  	hdrs := req.Header
   176  	for _, hdr := range []string{"Authorization", "x-subnet-license", "x-subnet-api-key"} {
   177  		if val := hdrs.Get(hdr); val != "" {
   178  			req.Header.Set(hdr, strings.Repeat("*", len(val)))
   179  		}
   180  	}
   181  
   182  	query := req.URL.Query()
   183  	for _, q := range []string{"api-key", "api_key"} {
   184  		if val := query.Get(q); val != "" {
   185  			query.Add(q, strings.Repeat("*", len(val)))
   186  		}
   187  	}
   188  	req.URL.RawQuery = query.Encode()
   189  
   190  	// Only display request header.
   191  	reqTrace, err := httputil.DumpRequestOut(req, false)
   192  	if err != nil {
   193  		return err
   194  	}
   195  
   196  	// Write request to trace output.
   197  	_, err = fmt.Fprint(os.Stderr, string(reqTrace))
   198  	if err != nil {
   199  		return err
   200  	}
   201  
   202  	respTrace, err := httputil.DumpResponse(resp, true)
   203  	if err != nil {
   204  		return err
   205  	}
   206  
   207  	// Write response to trace output.
   208  	_, err = fmt.Fprint(os.Stderr, strings.TrimSuffix(string(respTrace), "\r\n"))
   209  	if err != nil {
   210  		return err
   211  	}
   212  
   213  	// Ends the http dump.
   214  	_, err = fmt.Fprintln(os.Stderr, "---------END-HTTP---------")
   215  	return err
   216  }
   217  
   218  func subnetReqDo(r *http.Request, headers map[string]string) (string, error) {
   219  	for k, v := range headers {
   220  		r.Header.Add(k, v)
   221  	}
   222  
   223  	ct := r.Header.Get("Content-Type")
   224  	if len(ct) == 0 {
   225  		r.Header.Add("Content-Type", "application/json")
   226  	}
   227  
   228  	resp, e := subnetHTTPDo(r)
   229  	if e != nil {
   230  		return "", e
   231  	}
   232  
   233  	defer resp.Body.Close()
   234  	respBytes, e := io.ReadAll(io.LimitReader(resp.Body, subnetRespBodyLimit))
   235  	if e != nil {
   236  		return "", e
   237  	}
   238  	respStr := string(respBytes)
   239  
   240  	if resp.StatusCode == http.StatusOK {
   241  		return respStr, nil
   242  	}
   243  	return respStr, fmt.Errorf("Request failed with code %d with error: %s", resp.StatusCode, respStr)
   244  }
   245  
   246  func subnetHeadReq(reqURL string, headers map[string]string) (string, error) {
   247  	r, e := http.NewRequest(http.MethodHead, reqURL, nil)
   248  	if e != nil {
   249  		return "", e
   250  	}
   251  	return subnetReqDo(r, headers)
   252  }
   253  
   254  func subnetGetReq(reqURL string, headers map[string]string) (string, error) {
   255  	r, e := http.NewRequest(http.MethodGet, reqURL, nil)
   256  	if e != nil {
   257  		return "", e
   258  	}
   259  	return subnetReqDo(r, headers)
   260  }
   261  
   262  // SubnetPostReq - makes a POST request to SUBNET
   263  func SubnetPostReq(reqURL string, payload interface{}, headers map[string]string) (string, error) {
   264  	body, e := json.Marshal(payload)
   265  	if e != nil {
   266  		return "", e
   267  	}
   268  	r, e := http.NewRequest(http.MethodPost, reqURL, bytes.NewReader(body))
   269  	if e != nil {
   270  		return "", e
   271  	}
   272  	return subnetReqDo(r, headers)
   273  }
   274  
   275  func getMinIOSubSysConfig(client *madmin.AdminClient, subSys string) ([]madmin.SubsysConfig, error) {
   276  	buf, e := client.GetConfigKV(globalContext, subSys)
   277  	if e != nil {
   278  		return nil, e
   279  	}
   280  
   281  	return madmin.ParseServerConfigOutput(string(buf))
   282  }
   283  
   284  func getMinIOSubnetConfig(alias string) []madmin.SubsysConfig {
   285  	if globalSubnetConfig != nil {
   286  		return globalSubnetConfig
   287  	}
   288  
   289  	client, err := newAdminClient(alias)
   290  	fatalIf(err, "Unable to initialize admin connection.")
   291  
   292  	var e error
   293  	globalSubnetConfig, e = getMinIOSubSysConfig(client, madmin.SubnetSubSys)
   294  	if e != nil && e.Error() != "unknown sub-system subnet" {
   295  		fatal(probe.NewError(e), "Unable to get server config for subnet")
   296  	}
   297  
   298  	return globalSubnetConfig
   299  }
   300  
   301  func getKeyFromSubnetConfig(alias, key string) (string, bool) {
   302  	scfg := getMinIOSubnetConfig(alias)
   303  
   304  	// This function only works for fetch config from single target sub-systems
   305  	// in the server config and is enough for now.
   306  	if len(scfg) == 0 {
   307  		return "", false
   308  	}
   309  
   310  	return scfg[0].Lookup(key)
   311  }
   312  
   313  func getSubnetAPIKeyFromConfig(alias string) string {
   314  	// get the subnet api_key config from MinIO if available
   315  	apiKey, supported := getKeyFromSubnetConfig(alias, "api_key")
   316  	if supported {
   317  		return apiKey
   318  	}
   319  
   320  	// otherwise get it from mc config
   321  	return mcConfig().Aliases[alias].APIKey
   322  }
   323  
   324  func setGlobalSubnetProxyFromConfig(alias string) error {
   325  	if GlobalSubnetProxyURL != nil {
   326  		// proxy already set
   327  		return nil
   328  	}
   329  
   330  	var (
   331  		proxy     string
   332  		supported bool
   333  	)
   334  
   335  	if env, ok := os.LookupEnv("_MC_SUBNET_PROXY_URL"); ok {
   336  		proxy = env
   337  		supported = env != ""
   338  	} else {
   339  		proxy, supported = getKeyFromSubnetConfig(alias, "proxy")
   340  	}
   341  
   342  	// get the subnet proxy config from MinIO if available
   343  	if supported && len(proxy) > 0 {
   344  		proxyURL, e := url.Parse(proxy)
   345  		if e != nil {
   346  			return e
   347  		}
   348  		GlobalSubnetProxyURL = proxyURL
   349  	}
   350  	return nil
   351  }
   352  
   353  func getSubnetLicenseFromConfig(alias string) string {
   354  	// get the subnet license config from MinIO if available
   355  	lic, supported := getKeyFromSubnetConfig(alias, "license")
   356  	if supported {
   357  		return lic
   358  	}
   359  
   360  	// otherwise get it from mc config
   361  	return mcConfig().Aliases[alias].License
   362  }
   363  
   364  func mcConfig() *configV10 {
   365  	loadMcConfig = loadMcConfigFactory()
   366  	config, err := loadMcConfig()
   367  	fatalIf(err.Trace(mustGetMcConfigPath()), "Unable to access configuration file.")
   368  	return config
   369  }
   370  
   371  func minioConfigSupportsSubSys(client *madmin.AdminClient, subSys string) bool {
   372  	help, e := client.HelpConfigKV(globalContext, "", "", false)
   373  	fatalIf(probe.NewError(e), "Unable to get minio config keys")
   374  
   375  	for _, h := range help.KeysHelp {
   376  		if h.Key == subSys {
   377  			return true
   378  		}
   379  	}
   380  
   381  	return false
   382  }
   383  
   384  func setSubnetAPIKeyInMcConfig(alias, apiKey string) {
   385  	aliasCfg := mcConfig().Aliases[alias]
   386  	if len(apiKey) > 0 {
   387  		aliasCfg.APIKey = apiKey
   388  	}
   389  
   390  	setAlias(alias, aliasCfg)
   391  }
   392  
   393  func setSubnetLicenseInMcConfig(alias, lic string) {
   394  	aliasCfg := mcConfig().Aliases[alias]
   395  	if len(lic) > 0 {
   396  		aliasCfg.License = lic
   397  	}
   398  	setAlias(alias, aliasCfg)
   399  }
   400  
   401  func setSubnetConfig(alias, subKey, cfgVal string) {
   402  	client, err := newAdminClient(alias)
   403  	fatalIf(err, "Unable to initialize admin connection.")
   404  
   405  	cfgKey := "subnet " + subKey
   406  	_, e := client.SetConfigKV(globalContext, cfgKey+"="+cfgVal)
   407  	fatalIf(probe.NewError(e), "Unable to set "+cfgKey+" config on MinIO")
   408  }
   409  
   410  func setSubnetAPIKey(alias, apiKey string) {
   411  	if len(apiKey) == 0 {
   412  		fatal(errDummy().Trace(), "API Key must not be empty.")
   413  	}
   414  
   415  	_, apiKeySupported := getKeyFromSubnetConfig(alias, "api_key")
   416  	if !apiKeySupported {
   417  		setSubnetAPIKeyInMcConfig(alias, apiKey)
   418  		return
   419  	}
   420  
   421  	setSubnetConfig(alias, "api_key", apiKey)
   422  }
   423  
   424  func setSubnetLicense(alias, lic string) {
   425  	if len(lic) == 0 {
   426  		fatal(errDummy().Trace(), "License must not be empty.")
   427  	}
   428  
   429  	_, licSupported := getKeyFromSubnetConfig(alias, "license")
   430  	if !licSupported {
   431  		setSubnetLicenseInMcConfig(alias, lic)
   432  		return
   433  	}
   434  
   435  	setSubnetConfig(alias, "license", lic)
   436  }
   437  
   438  // GetClusterRegInfo - returns the cluster registration info
   439  func GetClusterRegInfo(admInfo madmin.InfoMessage, clusterName string) ClusterRegistrationInfo {
   440  	noOfPools := 1
   441  	noOfDrives := 0
   442  	for _, srvr := range admInfo.Servers {
   443  		for _, poolNumber := range srvr.PoolNumbers {
   444  			if poolNumber > noOfPools {
   445  				noOfPools = poolNumber
   446  			}
   447  		}
   448  		if len(srvr.PoolNumbers) == 0 {
   449  			if srvr.PoolNumber != math.MaxInt && srvr.PoolNumber > noOfPools {
   450  				noOfPools = srvr.PoolNumber
   451  			}
   452  		}
   453  		noOfDrives += len(srvr.Disks)
   454  	}
   455  
   456  	totalSpace, usedSpace := getDriveSpaceInfo(admInfo)
   457  
   458  	return ClusterRegistrationInfo{
   459  		DeploymentID: admInfo.DeploymentID,
   460  		ClusterName:  clusterName,
   461  		UsedCapacity: admInfo.Usage.Size,
   462  		Info: ClusterInfo{
   463  			MinioVersion:    admInfo.Servers[0].Version,
   464  			NoOfServerPools: noOfPools,
   465  			NoOfServers:     len(admInfo.Servers),
   466  			NoOfDrives:      noOfDrives,
   467  			TotalDriveSpace: totalSpace,
   468  			UsedDriveSpace:  usedSpace,
   469  			NoOfBuckets:     admInfo.Buckets.Count,
   470  			NoOfObjects:     admInfo.Objects.Count,
   471  		},
   472  	}
   473  }
   474  
   475  func getDriveSpaceInfo(admInfo madmin.InfoMessage) (uint64, uint64) {
   476  	total := uint64(0)
   477  	used := uint64(0)
   478  	for _, srvr := range admInfo.Servers {
   479  		for _, d := range srvr.Disks {
   480  			total += d.TotalSpace
   481  			used += d.UsedSpace
   482  		}
   483  	}
   484  	return total, used
   485  }
   486  
   487  func generateRegToken(clusterRegInfo ClusterRegistrationInfo) (string, error) {
   488  	token, e := json.Marshal(clusterRegInfo)
   489  	if e != nil {
   490  		return "", e
   491  	}
   492  
   493  	return base64.StdEncoding.EncodeToString(token), nil
   494  }
   495  
   496  func subnetLogin() (string, error) {
   497  	reader := bufio.NewReader(os.Stdin)
   498  	fmt.Print("SUBNET username: ")
   499  	username, _ := reader.ReadString('\n')
   500  	username = strings.TrimSpace(username)
   501  
   502  	if len(username) == 0 {
   503  		return "", errors.New("Username cannot be empty. If you don't have one, please create one from here: " + minioSubscriptionURL)
   504  	}
   505  
   506  	fmt.Print("Password: ")
   507  	bytepw, _ := term.ReadPassword(int(os.Stdin.Fd()))
   508  	fmt.Println()
   509  
   510  	loginReq := map[string]string{
   511  		"username": username,
   512  		"password": string(bytepw),
   513  	}
   514  	respStr, e := SubnetPostReq(subnetLoginURL(), loginReq, nil)
   515  	if e != nil {
   516  		return "", e
   517  	}
   518  
   519  	mfaRequired := gjson.Get(respStr, "mfa_required").Bool()
   520  	if mfaRequired {
   521  		mfaToken := gjson.Get(respStr, "mfa_token").String()
   522  		fmt.Print("OTP received in email: ")
   523  		byteotp, _ := term.ReadPassword(int(os.Stdin.Fd()))
   524  		fmt.Println()
   525  
   526  		mfaLoginReq := SubnetMFAReq{Username: username, OTP: string(byteotp), Token: mfaToken}
   527  		respStr, e = SubnetPostReq(subnetMFAURL(), mfaLoginReq, nil)
   528  		if e != nil {
   529  			return "", e
   530  		}
   531  	}
   532  
   533  	token := gjson.Get(respStr, "token_info.access_token")
   534  	if token.Exists() {
   535  		return token.String(), nil
   536  	}
   537  	return "", fmt.Errorf("access token not found in response")
   538  }
   539  
   540  // getSubnetCreds - returns the API key and license.
   541  // If only one of them is available, and if `--airgap` is not
   542  // passed, it will attempt to fetch the other from SUBNET
   543  // and save to config
   544  func getSubnetCreds(alias string) (string, string, error) {
   545  	apiKey := getSubnetAPIKeyFromConfig(alias)
   546  	lic := getSubnetLicenseFromConfig(alias)
   547  
   548  	if (len(apiKey) > 0 && len(lic) > 0) ||
   549  		(len(apiKey) == 0 && len(lic) == 0) ||
   550  		globalAirgapped {
   551  		return apiKey, lic, nil
   552  	}
   553  
   554  	var e error
   555  	// Not airgapped, and only one of api-key or license is available
   556  	// Try to fetch and save the other.
   557  	if len(apiKey) > 0 {
   558  		lic, e = getSubnetLicenseUsingAPIKey(alias, apiKey)
   559  	} else {
   560  		apiKey, e = getSubnetAPIKeyUsingLicense(lic)
   561  		if e == nil {
   562  			setSubnetAPIKey(alias, apiKey)
   563  		}
   564  	}
   565  
   566  	if e != nil {
   567  		return "", "", e
   568  	}
   569  
   570  	return apiKey, lic, nil
   571  }
   572  
   573  // getSubnetAPIKey - returns the SUBNET API key.
   574  // Returns error if the cluster is not registered with SUBNET.
   575  func getSubnetAPIKey(alias string) (string, error) {
   576  	apiKey, lic, e := getSubnetCreds(alias)
   577  	if e != nil {
   578  		return "", e
   579  	}
   580  	if len(apiKey) == 0 && len(lic) == 0 {
   581  		e = fmt.Errorf("Please register the cluster first by running 'mc license register %s'", alias)
   582  		return "", e
   583  	}
   584  	return apiKey, nil
   585  }
   586  
   587  func getSubnetAPIKeyUsingLicense(lic string) (string, error) {
   588  	return getSubnetAPIKeyUsingAuthHeaders(SubnetLicenseAuthHeaders(lic))
   589  }
   590  
   591  func getSubnetAPIKeyUsingAuthToken(authToken string) (string, error) {
   592  	return getSubnetAPIKeyUsingAuthHeaders(subnetTokenAuthHeaders(authToken))
   593  }
   594  
   595  func getSubnetAPIKeyUsingAuthHeaders(authHeaders map[string]string) (string, error) {
   596  	resp, e := subnetGetReq(subnetAPIKeyURL(), authHeaders)
   597  	if e != nil {
   598  		return "", e
   599  	}
   600  	return extractSubnetCred("api_key", gjson.Parse(resp))
   601  }
   602  
   603  func getSubnetLicenseUsingAPIKey(alias, apiKey string) (string, error) {
   604  	regInfo := GetClusterRegInfo(getAdminInfo(alias), alias)
   605  	_, lic, e := registerClusterOnSubnet(regInfo, alias, apiKey)
   606  	return lic, e
   607  }
   608  
   609  // registerClusterOnSubnet - Registers the given cluster on SUBNET using given API key for auth
   610  // If the API key is empty, user will be asked to log in using SUBNET credentials.
   611  func registerClusterOnSubnet(clusterRegInfo ClusterRegistrationInfo, alias, apiKey string) (string, string, error) {
   612  	regURL, headers, e := subnetURLWithAuth(SubnetRegisterURL(), apiKey)
   613  	if e != nil {
   614  		return "", "", e
   615  	}
   616  
   617  	regToken, e := generateRegToken(clusterRegInfo)
   618  	if e != nil {
   619  		return "", "", e
   620  	}
   621  
   622  	reqPayload := ClusterRegistrationReq{Token: regToken}
   623  	resp, e := SubnetPostReq(regURL, reqPayload, headers)
   624  	if e != nil {
   625  		return "", "", e
   626  	}
   627  
   628  	return extractAndSaveSubnetCreds(alias, resp)
   629  }
   630  
   631  func removeSubnetAuthConfig(alias string) {
   632  	setSubnetConfig(alias, "api_key", "")
   633  	setSubnetConfig(alias, "license", "")
   634  }
   635  
   636  // unregisterClusterFromSubnet - Unregisters the given cluster from SUBNET using given API key for auth
   637  func unregisterClusterFromSubnet(depID, apiKey string) error {
   638  	regURL, headers, e := subnetURLWithAuth(subnetUnregisterURL(depID), apiKey)
   639  	if e != nil {
   640  		return e
   641  	}
   642  
   643  	_, e = SubnetPostReq(regURL, nil, headers)
   644  	return e
   645  }
   646  
   647  // validateAndSaveLic - validates the given license in minio config
   648  // If the license contains api key and the saveApiKey arg is true,
   649  // api key is also saved in the minio config
   650  func validateAndSaveLic(lic, alias string, saveAPIKey bool) string {
   651  	li, e := parseLicense(lic)
   652  	fatalIf(probe.NewError(e), "Error parsing license")
   653  
   654  	if li.ExpiresAt.Before(time.Now()) {
   655  		fatalIf(errDummy().Trace(), fmt.Sprintf("License has expired on %s", li.ExpiresAt))
   656  	}
   657  
   658  	if len(li.DeploymentID) > 0 && li.DeploymentID != uuid.Nil.String() && li.DeploymentID != getAdminInfo(alias).DeploymentID {
   659  		fatalIf(errDummy().Trace(), fmt.Sprintf("License is invalid for the deployment %s", alias))
   660  	}
   661  
   662  	setSubnetLicense(alias, lic)
   663  	if len(li.APIKey) > 0 && saveAPIKey {
   664  		setSubnetAPIKey(alias, li.APIKey)
   665  	}
   666  
   667  	return li.APIKey
   668  }
   669  
   670  // extractAndSaveSubnetCreds - extract license from response and set it in minio config
   671  func extractAndSaveSubnetCreds(alias, resp string) (string, string, error) {
   672  	parsedResp := gjson.Parse(resp)
   673  
   674  	lic, e := extractSubnetCred("license_v2", parsedResp)
   675  	if e != nil {
   676  		return "", "", e
   677  	}
   678  	if len(lic) > 0 {
   679  		apiKey := validateAndSaveLic(lic, alias, true)
   680  		if len(apiKey) > 0 {
   681  			return apiKey, lic, nil
   682  		}
   683  	}
   684  
   685  	apiKey, e := extractSubnetCred("api_key", parsedResp)
   686  	if e != nil {
   687  		return "", "", e
   688  	}
   689  	if len(apiKey) > 0 {
   690  		setSubnetAPIKey(alias, apiKey)
   691  	}
   692  
   693  	return apiKey, lic, nil
   694  }
   695  
   696  func extractSubnetCred(key string, resp gjson.Result) (string, error) {
   697  	result := resp.Get(key)
   698  	if result.Index == 0 {
   699  		return "", fmt.Errorf("Couldn't extract %s from SUBNET response: %s", key, resp)
   700  	}
   701  	return result.String(), nil
   702  }
   703  
   704  // parseLicense parses the license with the bundle public key and return it's information
   705  func parseLicense(license string) (*licverifier.LicenseInfo, error) {
   706  	client := getSubnetClient()
   707  	lv := subnet.LicenseValidator{
   708  		Client:            *client,
   709  		ExpiryGracePeriod: 0,
   710  	}
   711  	lv.Init(GlobalDevMode)
   712  	return lv.ParseLicense(license)
   713  }
   714  
   715  func prepareSubnetUploadURL(uploadURL, alias, apiKey string) (string, map[string]string) {
   716  	var e error
   717  	if len(apiKey) == 0 {
   718  		// api key not passed as flag. check if it's available in the config
   719  		apiKey, e = getSubnetAPIKey(alias)
   720  		fatalIf(probe.NewError(e), "Unable to retrieve SUBNET API key")
   721  	}
   722  
   723  	reqURL, headers, e := subnetURLWithAuth(uploadURL, apiKey)
   724  	fatalIf(probe.NewError(e).Trace(uploadURL), "Unable to fetch SUBNET authentication")
   725  
   726  	return reqURL, headers
   727  }
   728  
   729  func getAPIKeyFlag(ctx *cli.Context) (string, error) {
   730  	apiKey := ctx.String("api-key")
   731  
   732  	if len(apiKey) == 0 {
   733  		return "", nil
   734  	}
   735  
   736  	_, e := uuid.Parse(apiKey)
   737  	if e != nil {
   738  		return "", e
   739  	}
   740  
   741  	return apiKey, nil
   742  }
   743  
   744  func initSubnetConnectivity(ctx *cli.Context, aliasedURL string, failOnConnErr bool) (string, string) {
   745  	if ctx.Bool("airgap") && len(ctx.String("api-key")) > 0 {
   746  		fatal(errDummy().Trace(), "--api-key is not applicable in airgap mode")
   747  	}
   748  
   749  	alias, _ := url2Alias(aliasedURL)
   750  
   751  	apiKey, e := getAPIKeyFlag(ctx)
   752  	fatalIf(probe.NewError(e), "Error in reading --api-key flag:")
   753  
   754  	// if `--airgap` is provided no need to test SUBNET connectivity.
   755  	if !globalAirgapped {
   756  		e = setGlobalSubnetProxyFromConfig(alias)
   757  		fatalIf(probe.NewError(e), "Error in setting SUBNET proxy:")
   758  
   759  		sbu := SubnetBaseURL()
   760  		err := checkURLReachable(sbu)
   761  		if err != nil && failOnConnErr {
   762  			fatal(err.Trace(aliasedURL), "Unable to reach %s, please use --airgap if there is no connectivity to SUBNET", sbu)
   763  		}
   764  	}
   765  
   766  	return alias, apiKey
   767  }