github.com/grafana/pyroscope@v1.18.0/cmd/profilecli/v2-migration.go (about)

     1  package main
     2  
     3  import (
     4  	"container/list"
     5  	"context"
     6  	"flag"
     7  	"fmt"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/grafana/dskit/flagext"
    12  	"golang.org/x/sync/errgroup"
    13  
    14  	pyroscopecfg "github.com/grafana/pyroscope/pkg/cfg"
    15  	pyroscopeobj "github.com/grafana/pyroscope/pkg/objstore"
    16  	objstoreclient "github.com/grafana/pyroscope/pkg/objstore/client"
    17  )
    18  
    19  type v2MigrationBucketCleanupParams struct {
    20  	configFile      string
    21  	configExpandEnv bool
    22  	dryRun          string
    23  }
    24  
    25  func (p *v2MigrationBucketCleanupParams) isDryRun() bool {
    26  	return p.dryRun != "false"
    27  }
    28  
    29  type minimalConfig struct {
    30  	Bucket objstoreclient.Config `yaml:"storage"`
    31  }
    32  
    33  // Note: These are not the flags used, but we need to register them to get the defaults.
    34  func (c *minimalConfig) RegisterFlags(f *flag.FlagSet) {
    35  	c.Bucket.RegisterFlags(f)
    36  }
    37  
    38  func (c *minimalConfig) ApplyDynamicConfig() pyroscopecfg.Source {
    39  	return func(dst pyroscopecfg.Cloneable) error {
    40  		return nil
    41  	}
    42  }
    43  
    44  func (c *minimalConfig) Clone() flagext.Registerer {
    45  	return func(c minimalConfig) *minimalConfig {
    46  		return &c
    47  	}(*c)
    48  }
    49  
    50  func clientFromParams(ctx context.Context, params *v2MigrationBucketCleanupParams) (pyroscopeobj.Bucket, error) {
    51  	if params.configFile == "" {
    52  		return nil, fmt.Errorf("config file is required")
    53  	}
    54  	cfg := &minimalConfig{}
    55  	fs := flag.NewFlagSet("config-file-loader", flag.ContinueOnError)
    56  	if err := pyroscopecfg.Unmarshal(cfg,
    57  		pyroscopecfg.Defaults(fs),
    58  		pyroscopecfg.YAMLIgnoreUnknownFields(params.configFile, params.configExpandEnv),
    59  	); err != nil {
    60  		return nil, fmt.Errorf("failed parsing config: %w", err)
    61  	}
    62  
    63  	return objstoreclient.NewBucket(ctx, cfg.Bucket, "profilecli")
    64  }
    65  
    66  func addV2MigrationBackupCleanupParam(c commander) *v2MigrationBucketCleanupParams {
    67  	var (
    68  		params = &v2MigrationBucketCleanupParams{}
    69  	)
    70  	c.Flag("config.file", "The path to the pyroscope config").Default("/etc/pyroscope/config.yaml").StringVar(&params.configFile)
    71  	c.Flag("config.expand-env", "").Default("false").BoolVar(&params.configExpandEnv)
    72  	c.Flag("dry-run", "Dry run the operation.").Default("true").StringVar(&params.dryRun)
    73  	return params
    74  }
    75  
    76  func v2MigrationBucketCleanup(ctx context.Context, params *v2MigrationBucketCleanupParams) error {
    77  	client, err := clientFromParams(ctx, params)
    78  	if err != nil {
    79  		return fmt.Errorf("failed to create client: %w", err)
    80  	}
    81  
    82  	var pathsToDelete []string
    83  	// find prefix called "phlaredb/" on the second level
    84  	if err := client.Iter(ctx, "", func(name string) error {
    85  		if !strings.HasSuffix(name, "/") {
    86  			return nil
    87  		}
    88  		err := client.Iter(ctx, name, func(name string) error {
    89  			if strings.HasSuffix(name, "phlaredb/") {
    90  				pathsToDelete = append(pathsToDelete, name)
    91  			}
    92  			return nil
    93  		})
    94  		if err != nil {
    95  			return err
    96  		}
    97  
    98  		return nil
    99  	}); err != nil {
   100  		return fmt.Errorf("failed to list tenants: %w", err)
   101  	}
   102  
   103  	if len(pathsToDelete) == 0 {
   104  		fmt.Println("No paths to delete")
   105  		return nil
   106  	}
   107  
   108  	if params.isDryRun() {
   109  		fmt.Println("DRY-RUN: If ran with --dry-run=false, this will delete everything under:")
   110  	} else {
   111  		fmt.Println("This will delete everything under:")
   112  	}
   113  	for _, path := range pathsToDelete {
   114  		fmt.Println(" - ", path)
   115  	}
   116  
   117  	if params.isDryRun() {
   118  		fmt.Println("DRY-RUN: If ran with --dry-run=false, this will delete those object store keys:")
   119  		return recurse(ctx, client, func(key string) error {
   120  			fmt.Println(" - ", key)
   121  			return nil
   122  		}, pathsToDelete)
   123  	}
   124  
   125  	// We do actually delete here
   126  	fmt.Println("Last chance to cancel, waiting 3 seconds...")
   127  	<-time.After(3 * time.Second)
   128  
   129  	fmt.Println("Deleted object store keys:")
   130  	return recurse(ctx, client, func(key string) error {
   131  		if err := client.Delete(ctx, key); err != nil {
   132  			return fmt.Errorf("failed to delete %s: %w", key, err)
   133  		}
   134  		fmt.Println(" - ", key)
   135  		return nil
   136  	}, pathsToDelete)
   137  }
   138  
   139  const maxConcurrentActions = 16
   140  
   141  func recurse(ctx context.Context, b pyroscopeobj.Bucket, action func(key string) error, paths []string) error {
   142  	g, gctx := errgroup.WithContext(ctx)
   143  	g.SetLimit(maxConcurrentActions)
   144  
   145  	g.Go(func() error {
   146  		iters := list.New()
   147  		for _, path := range paths {
   148  			iters.PushBack(path)
   149  		}
   150  
   151  		for iters.Len() > 0 {
   152  			e := iters.Front()
   153  			path := e.Value.(string)
   154  
   155  			if err := b.Iter(gctx, path, func(path string) error {
   156  				if strings.HasSuffix(path, "/") {
   157  					iters.PushBack(path)
   158  					return nil
   159  				}
   160  
   161  				g.Go(func() error {
   162  					return action(path)
   163  				})
   164  
   165  				return nil
   166  			}); err != nil {
   167  				return fmt.Errorf("failed to iterate over %s: %w", path, err)
   168  			}
   169  			iters.Remove(e)
   170  		}
   171  
   172  		return nil
   173  	})
   174  
   175  	return g.Wait()
   176  }