Files
s3-cli/cmd/rb-main.go
T
Anis ElleuchandGitHub 6b329ce01f rb: Fix side wide removal of an alias (#3796)
* rb: Fix side wide removal of an alias

rb --force --dangerous should remove all buckets under an alias. This
PR fixes a recent regression.
2021-10-06 09:56:57 -07:00

303 lines
8.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (c) 2015-2021 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package cmd
import (
"context"
"fmt"
"path"
"path/filepath"
"strings"
"github.com/fatih/color"
"github.com/minio/cli"
json "github.com/minio/colorjson"
"github.com/minio/mc/pkg/probe"
"github.com/minio/minio-go/v7"
"github.com/minio/pkg/console"
)
var (
rbFlags = []cli.Flag{
cli.BoolFlag{
Name: "force",
Usage: "force a recursive remove operation on all object versions",
},
cli.BoolFlag{
Name: "dangerous",
Usage: "allow site-wide removal of objects",
},
}
)
// remove a bucket.
var rbCmd = cli.Command{
Name: "rb",
Usage: "remove a bucket",
Action: mainRemoveBucket,
OnUsageError: onUsageError,
Before: setGlobalsFromContext,
Flags: append(rbFlags, globalFlags...),
CustomHelpTemplate: `NAME:
{{.HelpName}} - {{.Usage}}
USAGE:
{{.HelpName}} [FLAGS] TARGET [TARGET...]
{{if .VisibleFlags}}
FLAGS:
{{range .VisibleFlags}}{{.}}
{{end}}{{end}}
EXAMPLES:
1. Remove an empty bucket on Amazon S3 cloud storage
{{.Prompt}} {{.HelpName}} s3/mybucket
2. Remove a directory hierarchy.
{{.Prompt}} {{.HelpName}} /tmp/this/new/dir1
3. Remove bucket 'jazz-songs' and all its contents
{{.Prompt}} {{.HelpName}} --force s3/jazz-songs
4. Remove all buckets and objects recursively from S3 host
{{.Prompt}} {{.HelpName}} --force --dangerous s3
`,
}
// removeBucketMessage is container for delete bucket success and failure messages.
type removeBucketMessage struct {
Status string `json:"status"`
Bucket string `json:"bucket"`
}
// String colorized delete bucket message.
func (s removeBucketMessage) String() string {
return console.Colorize("RemoveBucket", fmt.Sprintf("Removed `%s` successfully.", s.Bucket))
}
// JSON jsonified remove bucket message.
func (s removeBucketMessage) JSON() string {
removeBucketJSONBytes, e := json.Marshal(s)
fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
return string(removeBucketJSONBytes)
}
// Validate command line arguments.
func checkRbSyntax(ctx context.Context, cliCtx *cli.Context) {
if !cliCtx.Args().Present() {
exitCode := 1
cli.ShowCommandHelpAndExit(cliCtx, "rb", exitCode)
}
// Set command flags from context.
isForce := cliCtx.Bool("force")
isDangerous := cliCtx.Bool("dangerous")
for _, url := range cliCtx.Args() {
if isS3NamespaceRemoval(ctx, url) {
if isForce && isDangerous {
continue
}
fatalIf(errDummy().Trace(),
"This operation results in **site-wide** removal of buckets. If you are really sure, retry this command with --force and --dangerous flags.")
}
}
}
// Return a list of aliased urls of buckets under the passed url
func listBucketsURLs(ctx context.Context, url string) ([]string, *probe.Error) {
var buckets []string
targetAlias, targetURL, _ := mustExpandAlias(url)
clnt, err := newClientFromAlias(targetAlias, targetURL)
if err != nil {
return nil, err
}
opts := ListOptions{
ShowDir: DirLast,
}
for content := range clnt.List(ctx, opts) {
if content.Err != nil {
errorIf(content.Err, "")
continue
}
select {
case <-ctx.Done():
return nil, probe.NewError(ctx.Err())
default:
}
bucketName := strings.TrimPrefix(content.URL.Path, clnt.GetURL().Path)
bucketPath := path.Join(url, bucketName)
buckets = append(buckets, bucketPath)
}
return buckets, nil
}
// Delete a bucket and all its objects and versions will be removed as well.
func deleteBucket(ctx context.Context, url string, isForce bool) *probe.Error {
targetAlias, targetURL, _ := mustExpandAlias(url)
clnt, pErr := newClientFromAlias(targetAlias, targetURL)
if pErr != nil {
return pErr
}
contentCh := make(chan *ClientContent)
errorCh := clnt.Remove(ctx, false, false, false, contentCh)
go func() {
defer close(contentCh)
opts := ListOptions{
Recursive: true,
WithOlderVersions: true,
WithDeleteMarkers: true,
ShowDir: DirLast,
}
for content := range clnt.List(ctx, opts) {
if content.Err != nil {
contentCh <- content
continue
}
urlString := content.URL.Path
select {
case contentCh <- content:
case <-ctx.Done():
return
}
// list internally mimics recursive directory listing of object prefixes for s3 similar to FS.
// The rmMessage needs to be printed only for actual buckets being deleted and not objects.
tgt := strings.TrimPrefix(urlString, string(filepath.Separator))
if !strings.Contains(tgt, string(filepath.Separator)) && tgt != targetAlias {
printMsg(removeBucketMessage{
Bucket: targetAlias + urlString, Status: "success",
})
}
}
}()
// Give up on the first error.
for perr := range errorCh {
return perr
}
// Remove a bucket without force flag first because force
// won't work if a bucket has some locking rules, that's
// why we start with regular bucket removal first.
err := clnt.RemoveBucket(ctx, false)
if err != nil {
if isForce && minio.ToErrorResponse(err.ToGoError()).Code == "BucketNotEmpty" {
return clnt.RemoveBucket(ctx, true)
}
}
return err
}
// isS3NamespaceRemoval returns true if alias
// is not qualified by bucket
func isS3NamespaceRemoval(ctx context.Context, url string) bool {
// clean path for aliases like s3/.
//Note: UNC path using / works properly in go 1.9.2 even though it breaks the UNC specification.
url = filepath.ToSlash(filepath.Clean(url))
// namespace removal applies only for non FS. So filter out if passed url represents a directory
_, path := url2Alias(url)
return (path == "")
}
// mainRemoveBucket is entry point for rb command.
func mainRemoveBucket(cliCtx *cli.Context) error {
ctx, cancelRemoveBucket := context.WithCancel(globalContext)
defer cancelRemoveBucket()
// check 'rb' cli arguments.
checkRbSyntax(ctx, cliCtx)
isForce := cliCtx.Bool("force")
// Additional command specific theme customization.
console.SetColor("RemoveBucket", color.New(color.FgGreen, color.Bold))
var cErr error
for _, targetURL := range cliCtx.Args() {
// Instantiate client for URL.
clnt, err := newClient(targetURL)
if err != nil {
errorIf(err.Trace(targetURL), "Invalid target `"+targetURL+"`.")
cErr = exitStatus(globalErrorExitStatus)
continue
}
_, err = clnt.Stat(ctx, StatOptions{})
if err != nil {
switch err.ToGoError().(type) {
case BucketNameEmpty:
default:
errorIf(err.Trace(targetURL), "Unable to validate target `"+targetURL+"`.")
cErr = exitStatus(globalErrorExitStatus)
continue
}
}
// Check if the bucket contains any object, version or delete marker.
isEmpty := true
opts := ListOptions{
Recursive: true,
ShowDir: DirNone,
WithOlderVersions: true,
WithDeleteMarkers: true,
}
listCtx, listCancel := context.WithCancel(ctx)
for obj := range clnt.List(listCtx, opts) {
if obj.Err != nil {
continue
}
isEmpty = false
break
}
listCancel()
// For all recursive operations make sure to check for 'force' flag.
if !isForce && !isEmpty {
fatalIf(errDummy().Trace(), "`"+targetURL+"` is not empty. Retry this command with --force flag if you want to remove `"+targetURL+"` and all its contents")
}
var bucketsURL []string
if isS3NamespaceRemoval(ctx, targetURL) {
bucketsURL, err = listBucketsURLs(ctx, targetURL)
fatalIf(err.Trace(targetURL), "Failed to remove `"+targetURL+"`.")
} else {
bucketsURL = []string{targetURL}
}
for _, bucketURL := range bucketsURL {
e := deleteBucket(ctx, bucketURL, isForce)
fatalIf(e.Trace(bucketURL), "Failed to remove `"+bucketURL+"`.")
printMsg(removeBucketMessage{
Bucket: bucketURL, Status: "success",
})
}
}
return cErr
}