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 }