github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/ilm-tier-add.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  	"fmt"
    22  	"os"
    23  	"strings"
    24  
    25  	"github.com/fatih/color"
    26  	"github.com/minio/cli"
    27  	json "github.com/minio/colorjson"
    28  	"github.com/minio/madmin-go/v3"
    29  	"github.com/minio/mc/pkg/probe"
    30  	"github.com/minio/pkg/v2/console"
    31  )
    32  
    33  var adminTierAddFlags = []cli.Flag{
    34  	cli.StringFlag{
    35  		Name:  "endpoint",
    36  		Value: "",
    37  		Usage: "remote tier endpoint. e.g https://s3.amazonaws.com",
    38  	},
    39  	cli.StringFlag{
    40  		Name:  "region",
    41  		Value: "",
    42  		Usage: "remote tier region. e.g us-west-2",
    43  	},
    44  	cli.StringFlag{
    45  		Name:  "access-key",
    46  		Value: "",
    47  		Usage: "AWS S3 or compatible object storage access-key",
    48  	},
    49  	cli.StringFlag{
    50  		Name:  "secret-key",
    51  		Value: "",
    52  		Usage: "AWS S3 or compatible object storage secret-key",
    53  	},
    54  	cli.BoolFlag{
    55  		Name:  "use-aws-role",
    56  		Usage: "use AWS S3 role",
    57  	},
    58  	cli.StringFlag{
    59  		Name:  "aws-role-arn",
    60  		Usage: "use AWS S3 role name",
    61  	},
    62  	cli.StringFlag{
    63  		Name:  "aws-web-identity-file",
    64  		Usage: "use AWS S3 web identity file",
    65  	},
    66  	cli.StringFlag{
    67  		Name:  "account-name",
    68  		Value: "",
    69  		Usage: "Azure Blob Storage account name",
    70  	},
    71  	cli.StringFlag{
    72  		Name:  "account-key",
    73  		Value: "",
    74  		Usage: "Azure Blob Storage account key",
    75  	},
    76  	cli.StringFlag{
    77  		Name:  "az-sp-tenant-id",
    78  		Value: "",
    79  		Usage: "Directory ID for the Azure service principal account",
    80  	},
    81  	cli.StringFlag{
    82  		Name:  "az-sp-client-id",
    83  		Value: "",
    84  		Usage: "The client ID of the Azure service principal account",
    85  	},
    86  	cli.StringFlag{
    87  		Name:  "az-sp-client-secret",
    88  		Value: "",
    89  		Usage: "The client secret of the Azure service principal account",
    90  	},
    91  	cli.StringFlag{
    92  		Name:  "credentials-file",
    93  		Value: "",
    94  		Usage: "path to Google Cloud Storage credentials file",
    95  	},
    96  	cli.StringFlag{
    97  		Name:  "bucket",
    98  		Value: "",
    99  		Usage: "remote tier bucket",
   100  	},
   101  	cli.StringFlag{
   102  		Name:  "prefix",
   103  		Value: "",
   104  		Usage: "remote tier prefix",
   105  	},
   106  	cli.StringFlag{
   107  		Name:  "storage-class",
   108  		Value: "",
   109  		Usage: "remote tier storage-class",
   110  	},
   111  	cli.BoolFlag{
   112  		Name:   "force",
   113  		Hidden: true,
   114  		Usage:  "ignores in-use check for remote tier bucket/prefix",
   115  	},
   116  }
   117  
   118  var adminTierAddCmd = cli.Command{
   119  	Name:         "add",
   120  	Usage:        "add a new remote tier target",
   121  	Action:       mainAdminTierAdd,
   122  	OnUsageError: onUsageError,
   123  	Before:       setGlobalsFromContext,
   124  	Flags:        append(globalFlags, adminTierAddFlags...),
   125  	CustomHelpTemplate: `NAME:
   126    {{.HelpName}} - {{.Usage}}
   127  
   128  USAGE:
   129    {{.HelpName}} TYPE ALIAS NAME [FLAGS]
   130  
   131  TYPE:
   132    Transition objects to supported cloud storage backend tier. Supported values are minio, s3, azure and gcs.
   133  
   134  NAME:
   135    Name of the remote tier target. e.g WARM-TIER
   136  
   137  FLAGS:
   138    {{range .VisibleFlags}}{{.}}
   139    {{end}}
   140  EXAMPLES:
   141    1. Configure a new remote tier which transitions objects to a bucket in a MinIO deployment:
   142       {{.Prompt}} {{.HelpName}} minio myminio WARM-MINIO-TIER --endpoint https://warm-minio.com \
   143          --access-key ACCESSKEY --secret-key SECRETKEY --bucket mybucket --prefix myprefix/
   144  
   145    2. Configure a new remote tier which transitions objects to a bucket in Azure Blob Storage:
   146       {{.Prompt}} {{.HelpName}} azure myminio AZTIER --account-name ACCOUNT-NAME --account-key ACCOUNT-KEY \
   147          --bucket myazurebucket --prefix myazureprefix/
   148  
   149    3. Configure a new remote tier which transitions objects to a bucket in AWS S3 with STANDARD storage class:
   150       {{.Prompt}} {{.HelpName}} s3 myminio S3TIER --endpoint https://s3.amazonaws.com \
   151          --access-key ACCESSKEY --secret-key SECRETKEY --bucket mys3bucket --prefix mys3prefix/ \
   152          --storage-class "STANDARD" --region us-west-2
   153  
   154    4. Configure a new remote tier which transitions objects to a bucket in Google Cloud Storage:
   155       {{.Prompt}} {{.HelpName}} gcs myminio GCSTIER --credentials-file /path/to/credentials.json \
   156          --bucket mygcsbucket  --prefix mygcsprefix/
   157  `,
   158  }
   159  
   160  // checkAdminTierAddSyntax validates all the positional arguments
   161  func checkAdminTierAddSyntax(ctx *cli.Context) {
   162  	argsNr := len(ctx.Args())
   163  	if argsNr < 3 {
   164  		showCommandHelpAndExit(ctx, 1) // last argument is exit code
   165  	}
   166  	if argsNr > 3 {
   167  		fatalIf(errInvalidArgument().Trace(ctx.Args().Tail()...),
   168  			"Incorrect number of arguments for tier add command.")
   169  	}
   170  }
   171  
   172  const (
   173  	s3Standard          = "STANDARD"
   174  	s3ReducedRedundancy = "REDUCED_REDUNDANCY"
   175  )
   176  
   177  // fetchTierConfig returns a TierConfig given a tierName, a tierType and ctx to
   178  // lookup command-line flags from. It exits with non-zero error code if any of
   179  // the flags contain invalid values.
   180  func fetchTierConfig(ctx *cli.Context, tierName string, tierType madmin.TierType) *madmin.TierConfig {
   181  	switch tierType {
   182  	case madmin.MinIO:
   183  		accessKey := ctx.String("access-key")
   184  		secretKey := ctx.String("secret-key")
   185  		if accessKey == "" || secretKey == "" {
   186  			fatalIf(errInvalidArgument().Trace(), fmt.Sprintf("%s remote tier requires access credentials", tierType))
   187  		}
   188  		bucket := ctx.String("bucket")
   189  		if bucket == "" {
   190  			fatalIf(errInvalidArgument().Trace(), fmt.Sprintf("%s remote tier requires target bucket", tierType))
   191  		}
   192  
   193  		endpoint := ctx.String("endpoint")
   194  		if endpoint == "" {
   195  			fatalIf(errInvalidArgument().Trace(), fmt.Sprintf("%s remote tier requires target endpoint", tierType))
   196  		}
   197  
   198  		minioOpts := []madmin.MinIOOptions{}
   199  		prefix := ctx.String("prefix")
   200  		if prefix != "" {
   201  			minioOpts = append(minioOpts, madmin.MinIOPrefix(prefix))
   202  		}
   203  
   204  		region := ctx.String("region")
   205  		if region != "" {
   206  			minioOpts = append(minioOpts, madmin.MinIORegion(region))
   207  		}
   208  
   209  		minioCfg, e := madmin.NewTierMinIO(tierName, endpoint, accessKey, secretKey, bucket, minioOpts...)
   210  		fatalIf(probe.NewError(e), "Invalid configuration for MinIO tier")
   211  
   212  		return minioCfg
   213  
   214  	case madmin.S3:
   215  		accessKey := ctx.IsSet("access-key")
   216  		secretKey := ctx.IsSet("secret-key")
   217  		useAwsRole := ctx.IsSet("use-aws-role")
   218  		awsRoleArn := ctx.IsSet("aws-role-arn")
   219  		awsWebIdentity := ctx.IsSet("aws-web-identity-file")
   220  
   221  		// Extensive flag check
   222  		switch {
   223  		case !accessKey && !secretKey && !useAwsRole && !awsRoleArn && !awsWebIdentity:
   224  			fatalIf(errInvalidArgument().Trace(), fmt.Sprintf("%s: No authentication mechanism was provided", tierType))
   225  		case (accessKey || secretKey) && (useAwsRole || awsRoleArn || awsWebIdentity):
   226  			fatalIf(errInvalidArgument().Trace(), fmt.Sprintf("%s: Static credentials cannot be combined with AWS role authentication", tierType))
   227  		case useAwsRole && (awsRoleArn || awsWebIdentity):
   228  			fatalIf(errInvalidArgument().Trace(), fmt.Sprintf("%s: --use-aws-role cannot be combined with --aws-role-arn or --aws-web-identity-file", tierType))
   229  		case (awsRoleArn && !awsWebIdentity) || (!awsRoleArn && awsWebIdentity):
   230  			fatalIf(errInvalidArgument().Trace(), fmt.Sprintf("%s: Both --use-aws-role and --aws-web-identity-file are required to enable web identity token based authentication", tierType))
   231  		case (accessKey && !secretKey) || (!accessKey && secretKey):
   232  			fatalIf(errInvalidArgument().Trace(), fmt.Sprintf("%s: Both --access-key and --secret-key are required to enable static credentials authentication", tierType))
   233  
   234  		}
   235  
   236  		bucket := ctx.String("bucket")
   237  		if bucket == "" {
   238  			fatalIf(errInvalidArgument().Trace(), fmt.Sprintf("%s remote tier requires target bucket", tierType))
   239  		}
   240  
   241  		s3Opts := []madmin.S3Options{}
   242  		prefix := ctx.String("prefix")
   243  		if prefix != "" {
   244  			s3Opts = append(s3Opts, madmin.S3Prefix(prefix))
   245  		}
   246  
   247  		endpoint := ctx.String("endpoint")
   248  		if endpoint != "" {
   249  			s3Opts = append(s3Opts, madmin.S3Endpoint(endpoint))
   250  		}
   251  
   252  		region := ctx.String("region")
   253  		if region != "" {
   254  			s3Opts = append(s3Opts, madmin.S3Region(region))
   255  		}
   256  
   257  		s3SC := ctx.String("storage-class")
   258  		if s3SC != "" {
   259  			if s3SC != s3Standard && s3SC != s3ReducedRedundancy {
   260  				fatalIf(errInvalidArgument().Trace(), fmt.Sprintf("unsupported storage-class type %s", s3SC))
   261  			}
   262  			s3Opts = append(s3Opts, madmin.S3StorageClass(s3SC))
   263  		}
   264  		if ctx.IsSet("use-aws-role") {
   265  			s3Opts = append(s3Opts, madmin.S3AWSRole())
   266  		}
   267  		if ctx.IsSet("aws-role-arn") {
   268  			s3Opts = append(s3Opts, madmin.S3AWSRoleARN(ctx.String("aws-role-arn")))
   269  		}
   270  		if ctx.IsSet("aws-web-identity-file") {
   271  			s3Opts = append(s3Opts, madmin.S3AWSRoleWebIdentityTokenFile(ctx.String("aws-web-identity-file")))
   272  		}
   273  		s3Cfg, e := madmin.NewTierS3(tierName, ctx.String("access-key"), ctx.String("secret-key"), bucket, s3Opts...)
   274  		fatalIf(probe.NewError(e), "Invalid configuration for AWS S3 compatible remote tier")
   275  
   276  		return s3Cfg
   277  	case madmin.Azure:
   278  		accountName := ctx.String("account-name")
   279  		accountKey := ctx.String("account-key")
   280  		if accountName == "" {
   281  			fatalIf(errDummy().Trace(), fmt.Sprintf("%s remote tier requires the storage account name", tierType))
   282  		}
   283  
   284  		if accountKey == "" && (ctx.String("az-sp-tenant-id") == "" || ctx.String("az-sp-client-id") == "" || ctx.String("az-sp-client-secret") == "") {
   285  			fatalIf(errDummy().Trace(), fmt.Sprintf("%s remote tier requires static credentials OR service principal credentials", tierType))
   286  		}
   287  
   288  		bucket := ctx.String("bucket")
   289  		if bucket == "" {
   290  			fatalIf(errDummy().Trace(), fmt.Sprintf("%s remote tier requires target bucket", tierType))
   291  		}
   292  
   293  		azOpts := []madmin.AzureOptions{}
   294  		endpoint := ctx.String("endpoint")
   295  		if endpoint != "" {
   296  			azOpts = append(azOpts, madmin.AzureEndpoint(endpoint))
   297  		}
   298  
   299  		region := ctx.String("region")
   300  		if region != "" {
   301  			azOpts = append(azOpts, madmin.AzureRegion(region))
   302  		}
   303  
   304  		prefix := ctx.String("prefix")
   305  		if prefix != "" {
   306  			azOpts = append(azOpts, madmin.AzurePrefix(prefix))
   307  		}
   308  
   309  		if ctx.String("az-sp-tenant-id") != "" || ctx.String("az-sp-client-id") != "" || ctx.String("az-sp-client-secret") != "" {
   310  			azOpts = append(azOpts, madmin.AzureServicePrincipal(ctx.String("az-sp-tenant-id"), ctx.String("az-sp-client-id"), ctx.String("az-sp-client-secret")))
   311  		}
   312  
   313  		azCfg, e := madmin.NewTierAzure(tierName, accountName, accountKey, bucket, azOpts...)
   314  		fatalIf(probe.NewError(e), "Invalid configuration for Azure Blob Storage remote tier")
   315  
   316  		return azCfg
   317  	case madmin.GCS:
   318  		bucket := ctx.String("bucket")
   319  		if bucket == "" {
   320  			fatalIf(errInvalidArgument().Trace(), fmt.Sprintf("%s remote requires target bucket", tierType))
   321  		}
   322  
   323  		gcsOpts := []madmin.GCSOptions{}
   324  		prefix := ctx.String("prefix")
   325  		if prefix != "" {
   326  			gcsOpts = append(gcsOpts, madmin.GCSPrefix(prefix))
   327  		}
   328  
   329  		region := ctx.String("region")
   330  		if region != "" {
   331  			gcsOpts = append(gcsOpts, madmin.GCSRegion(region))
   332  		}
   333  
   334  		credsPath := ctx.String("credentials-file")
   335  		credsBytes, e := os.ReadFile(credsPath)
   336  		fatalIf(probe.NewError(e), "Failed to read credentials file")
   337  
   338  		gcsCfg, e := madmin.NewTierGCS(tierName, credsBytes, bucket, gcsOpts...)
   339  		fatalIf(probe.NewError(e), "Invalid configuration for Google Cloud Storage remote tier")
   340  
   341  		return gcsCfg
   342  	}
   343  	fatalIf(errInvalidArgument().Trace(), fmt.Sprintf("Invalid remote tier type %s", tierType))
   344  	return nil
   345  }
   346  
   347  type tierMessage struct {
   348  	op         string
   349  	Status     string            `json:"status"`
   350  	TierName   string            `json:"tierName"`
   351  	TierType   string            `json:"tierType"`
   352  	Endpoint   string            `json:"tierEndpoint"`
   353  	Bucket     string            `json:"bucket"`
   354  	Prefix     string            `json:"prefix,omitempty"`
   355  	Region     string            `json:"region,omitempty"`
   356  	TierParams map[string]string `json:"tierParams,omitempty"`
   357  }
   358  
   359  // String returns string representation of msg
   360  func (msg *tierMessage) String() string {
   361  	switch msg.op {
   362  	case "add":
   363  		addMsg := fmt.Sprintf("Added remote tier %s of type %s", msg.TierName, msg.TierType)
   364  		return console.Colorize("TierMessage", addMsg)
   365  	case "rm":
   366  		rmMsg := fmt.Sprintf("Removed remote tier %s", msg.TierName)
   367  		return console.Colorize("TierMessage", rmMsg)
   368  	case "verify":
   369  		verifyMsg := fmt.Sprintf("Verified remote tier %s", msg.TierName)
   370  		return console.Colorize("TierMessage", verifyMsg)
   371  	case "check":
   372  		checkMsg := fmt.Sprintf("Remote tier connectivity check for %s was successful", msg.TierName)
   373  		return console.Colorize("TierMessage", checkMsg)
   374  	case "edit":
   375  		editMsg := fmt.Sprintf("Updated remote tier %s", msg.TierName)
   376  		return console.Colorize("TierMessage", editMsg)
   377  	}
   378  	return ""
   379  }
   380  
   381  // JSON returns json encoded msg
   382  func (msg *tierMessage) JSON() string {
   383  	jsonMessageBytes, e := json.MarshalIndent(msg, "", " ")
   384  	fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
   385  
   386  	return string(jsonMessageBytes)
   387  }
   388  
   389  // SetTierConfig sets TierConfig related fields
   390  func (msg *tierMessage) SetTierConfig(sCfg *madmin.TierConfig) {
   391  	msg.TierName = sCfg.Name
   392  	msg.TierType = sCfg.Type.String()
   393  	msg.Endpoint = sCfg.Endpoint()
   394  	msg.Bucket = sCfg.Bucket()
   395  	msg.Prefix = sCfg.Prefix()
   396  	msg.Region = sCfg.Region()
   397  	switch sCfg.Type {
   398  	case madmin.S3:
   399  		msg.TierParams = map[string]string{
   400  			"storageClass": sCfg.S3.StorageClass,
   401  		}
   402  	}
   403  }
   404  
   405  func mainAdminTierAdd(ctx *cli.Context) error {
   406  	checkAdminTierAddSyntax(ctx)
   407  
   408  	console.SetColor("TierMessage", color.New(color.FgGreen))
   409  
   410  	args := ctx.Args()
   411  	tierTypeStr := args.Get(0)
   412  	tierType, e := madmin.NewTierType(tierTypeStr)
   413  	fatalIf(probe.NewError(e), "Unsupported tier type")
   414  
   415  	aliasedURL := args.Get(1)
   416  	tierName := args.Get(2)
   417  	if tierName == "" {
   418  		fatalIf(errInvalidArgument(), "Tier name can't be empty")
   419  	}
   420  
   421  	// Create a new MinIO Admin Client
   422  	client, cerr := newAdminClient(aliasedURL)
   423  	fatalIf(cerr, "Unable to initialize admin connection.")
   424  
   425  	tCfg := fetchTierConfig(ctx, strings.ToUpper(tierName), tierType)
   426  	ignoreInUse := ctx.Bool("force")
   427  	if ignoreInUse {
   428  		fatalIf(probe.NewError(client.AddTierIgnoreInUse(globalContext, tCfg)).Trace(args...), "Unable to configure remote tier target")
   429  	} else {
   430  		fatalIf(probe.NewError(client.AddTier(globalContext, tCfg)).Trace(args...), "Unable to configure remote tier target")
   431  	}
   432  
   433  	msg := &tierMessage{
   434  		op:     ctx.Command.Name,
   435  		Status: "success",
   436  	}
   437  	msg.SetTierConfig(tCfg)
   438  	printMsg(msg)
   439  	return nil
   440  }