mirror of
https://github.com/containers/skopeo.git
synced 2025-08-18 06:27:27 +00:00
feat(prune): filter by date, skip summary and noninteractive opts
Signed-off-by: Ethan Balcik <ebalcik71@gmail.com>
This commit is contained in:
parent
3585b8de8f
commit
10b95819ba
@ -11,6 +11,7 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
commonFlag "github.com/containers/common/pkg/flag"
|
||||
"github.com/containers/common/pkg/retry"
|
||||
@ -47,6 +48,7 @@ func newFilteredTags() *filteredTags {
|
||||
|
||||
type tagFilterOptions struct {
|
||||
BeforeVersion commonFlag.OptionalString
|
||||
BeforeTime commonFlag.OptionalString
|
||||
VersionLabel commonFlag.OptionalString
|
||||
Valid commonFlag.OptionalBool
|
||||
Invalid commonFlag.OptionalBool
|
||||
@ -54,13 +56,18 @@ type tagFilterOptions struct {
|
||||
}
|
||||
|
||||
func (opts *tagFilterOptions) FilterPresent() bool {
|
||||
return opts.BeforeVersion.Present() || opts.Valid.Present() || opts.Invalid.Present()
|
||||
return opts.BeforeVersion.Present() || opts.Valid.Present() || opts.Invalid.Present() || opts.BeforeTime.Present()
|
||||
}
|
||||
|
||||
func (opts *tagFilterOptions) InspectFilterPresent() bool {
|
||||
return (opts.BeforeVersion.Present() && opts.VersionLabel.Present()) || opts.BeforeTime.Present()
|
||||
}
|
||||
|
||||
func filterFlags() (pflag.FlagSet, *tagFilterOptions) {
|
||||
opts := tagFilterOptions{}
|
||||
fs := pflag.FlagSet{}
|
||||
fs.Var(commonFlag.NewOptionalStringValue(&opts.BeforeVersion), "before-version", "A version threshold prior to which to list tags")
|
||||
fs.Var(commonFlag.NewOptionalStringValue(&opts.BeforeTime), "before-time", "A date threshold prior to which to list tags")
|
||||
fs.Var(commonFlag.NewOptionalStringValue(&opts.VersionLabel), "version-label", "A label from which to derive the version for each tag")
|
||||
commonFlag.OptionalBoolFlag(&fs, &opts.Valid, "valid", "Whether to list only tags with valid semver")
|
||||
commonFlag.OptionalBoolFlag(&fs, &opts.Invalid, "invalid", "Whether to list only tags with invalid semver")
|
||||
@ -190,6 +197,16 @@ func filterDockerTagBySemver(filtered *filteredTags, opts *tagsOptions, threshol
|
||||
return nil
|
||||
}
|
||||
|
||||
func filterDockerTagByDate(filtered *filteredTags, opts *tagsOptions, timeThreshold time.Time, tag string, created time.Time) (error) {
|
||||
// Compare the created date against the threshold date
|
||||
if created.Before(timeThreshold) {
|
||||
filtered.ToPrune = append(filtered.ToPrune, tag)
|
||||
} else {
|
||||
filtered.ToKeep = append(filtered.ToKeep, tag)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func filterDockerTagsByTagSemver(opts *tagsOptions, tags *tagListOutput) (*filteredTags, error) {
|
||||
// Get the user-provided threshold version
|
||||
// This will be validated later when the comparison takes place
|
||||
@ -213,15 +230,15 @@ func filterDockerTagsByTagSemver(opts *tagsOptions, tags *tagListOutput) (*filte
|
||||
}
|
||||
|
||||
// Use goroutines to parallelize & display progress continuously
|
||||
func filterDockerTagsByLabelSemver(ctx context.Context, sys *types.SystemContext, opts *tagsOptions, tags *tagListOutput) (*filteredTags, error) {
|
||||
func filterDockerTagsByImageMetadata(ctx context.Context, sys *types.SystemContext, opts *tagsOptions, tags *tagListOutput) (*filteredTags, error) {
|
||||
// Get the user-provided threshold version
|
||||
// This will be validated later when the comparison takes place
|
||||
var threshold string
|
||||
var versionThreshold string
|
||||
if opts.filterOpts.BeforeVersion.Present() {
|
||||
threshold = opts.filterOpts.BeforeVersion.Value()
|
||||
versionThreshold = opts.filterOpts.BeforeVersion.Value()
|
||||
} else {
|
||||
// Set as an arbitrary valid version since this isn't going to affect output
|
||||
threshold = "v0.1.0"
|
||||
versionThreshold = "v0.1.0"
|
||||
}
|
||||
|
||||
// Initialize a zeroed filteredTags struct to return
|
||||
@ -255,13 +272,13 @@ func filterDockerTagsByLabelSemver(ctx context.Context, sys *types.SystemContext
|
||||
if len(msg) > 0 {
|
||||
// Log progress in-place
|
||||
tagsFiltered += 1
|
||||
fmt.Printf("\rfetching image labels\t%d / %d", tagsFiltered, totalTags)
|
||||
fmt.Fprintf(os.Stderr, "\rinspecting image tags\t%d / %d", tagsFiltered, totalTags)
|
||||
}
|
||||
case err := <-errChan:
|
||||
// Print the error and append to the errors slice
|
||||
if err != nil {
|
||||
numErrors += 1
|
||||
fmt.Fprintf(os.Stderr, "\nerror while fetching image labels %s\n", err)
|
||||
fmt.Fprintf(os.Stderr, "\nerror while inspecting image tags %s\n", err)
|
||||
}
|
||||
case <-doneChan:
|
||||
break filterLoop
|
||||
@ -269,11 +286,11 @@ func filterDockerTagsByLabelSemver(ctx context.Context, sys *types.SystemContext
|
||||
}
|
||||
|
||||
// Log completion in-place and close the goroutine
|
||||
fmt.Printf("\rfetching image labels\t%d / %d (done)\n", tagsFiltered, totalTags)
|
||||
fmt.Fprintf(os.Stderr, "\rinspecting image tags\t%d / %d (done)\n", tagsFiltered, totalTags)
|
||||
readWaitGroup.Done()
|
||||
}(filteredChan, errChan, doneChan)
|
||||
|
||||
// Goroutines for fetching image labels
|
||||
// Goroutines for fetching image metadata
|
||||
var fetchWaitGroup = sync.WaitGroup{}
|
||||
for i := 0; i < len(tags.Tags); i++ {
|
||||
fetchWaitGroup.Add(1)
|
||||
@ -319,17 +336,31 @@ func filterDockerTagsByLabelSemver(ctx context.Context, sys *types.SystemContext
|
||||
return
|
||||
}
|
||||
|
||||
// Get the version label and filter it into the filteredTags struct
|
||||
versionLabel := opts.filterOpts.VersionLabel.Value()
|
||||
tagVersion, ok := imgInspect.Labels[versionLabel]
|
||||
if !ok {
|
||||
errChan <- fmt.Errorf("For tag %s: version label not found: %s", tag, versionLabel)
|
||||
return
|
||||
}
|
||||
err = filterDockerTagBySemver(filtered, opts, threshold, tag, tagVersion)
|
||||
if err != nil {
|
||||
errChan <- fmt.Errorf("Error filtering tags: %w", err)
|
||||
return
|
||||
if opts.filterOpts.VersionLabel.Present() {
|
||||
// If there is a version label threshold, then filter by the version label
|
||||
versionLabel := opts.filterOpts.VersionLabel.Value()
|
||||
tagVersion, ok := imgInspect.Labels[versionLabel]
|
||||
if !ok {
|
||||
errChan <- fmt.Errorf("For tag %s: version label not found: %s", tag, versionLabel)
|
||||
return
|
||||
}
|
||||
err = filterDockerTagBySemver(filtered, opts, versionThreshold, tag, tagVersion)
|
||||
if err != nil {
|
||||
errChan <- fmt.Errorf("For tag %s: error filtering: %w", tag, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// If there is a time threshold, then filter by the created date
|
||||
timeThreshold, err := time.Parse(time.RFC3339, opts.filterOpts.BeforeTime.Value())
|
||||
if err != nil {
|
||||
errChan <- fmt.Errorf("Error parsing time threshold for filtering: %w", err)
|
||||
return
|
||||
}
|
||||
err = filterDockerTagByDate(filtered, opts, timeThreshold, tag, *imgInspect.Created)
|
||||
if err != nil {
|
||||
errChan <- fmt.Errorf("For tag %s: error filtering: %w", tag, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// If successful, then signal completion
|
||||
@ -357,7 +388,7 @@ func filterDockerTagsByLabelSemver(ctx context.Context, sys *types.SystemContext
|
||||
|
||||
// Return the filtered tags, and an error summary if any errors occurred
|
||||
if numErrors > 0 {
|
||||
return filtered, fmt.Errorf("Encountered %d errors while filtering tags by label semver", numErrors)
|
||||
return filtered, fmt.Errorf("Encountered %d errors while filtering tags by inspect metadata", numErrors)
|
||||
}
|
||||
return filtered, nil
|
||||
}
|
||||
@ -367,8 +398,8 @@ func filterDockerTags(ctx context.Context, sys *types.SystemContext, opts *tagsO
|
||||
Repository: repositoryName,
|
||||
Tags: tags,
|
||||
}
|
||||
if opts.filterOpts.VersionLabel.Present() {
|
||||
return filterDockerTagsByLabelSemver(ctx, sys, opts, tagList)
|
||||
if opts.filterOpts.InspectFilterPresent() {
|
||||
return filterDockerTagsByImageMetadata(ctx, sys, opts, tagList)
|
||||
}
|
||||
return filterDockerTagsByTagSemver(opts, tagList)
|
||||
}
|
||||
@ -378,8 +409,8 @@ func listFilteredDockerTags(ctx context.Context, sys *types.SystemContext, opts
|
||||
if err != nil {
|
||||
return ``, nil, fmt.Errorf("Error filtering tags by semver: %w", err)
|
||||
}
|
||||
if opts.filterOpts.BeforeVersion.Present() {
|
||||
// Optionally only list tags prior to given semver threshold
|
||||
if opts.filterOpts.BeforeVersion.Present() || opts.filterOpts.BeforeTime.Present() {
|
||||
// Optionally only list tags prior to given semver or date threshold
|
||||
return repositoryName, filtered.ToPrune, nil
|
||||
} else if opts.filterOpts.Invalid.Present() {
|
||||
// Optionally only list tags with invalid semver
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
"github.com/containers/image/v5/transports/alltransports"
|
||||
"github.com/containers/image/v5/types"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// Enumerated constant for byte units
|
||||
@ -76,7 +77,7 @@ func bytesToByteUnit(bytes int64) string {
|
||||
return fmt.Sprintf("%.2f %s", unitDec, unitSuf)
|
||||
}
|
||||
|
||||
var pruneTransportHandlers = map[string]func(ctx context.Context, sys *types.SystemContext, opts *tagsOptions, userInput string) error {
|
||||
var pruneTransportHandlers = map[string]func(ctx context.Context, sys *types.SystemContext, opts *pruneOptions, userInput string) error {
|
||||
docker.Transport.Name(): pruneDockerTags,
|
||||
}
|
||||
|
||||
@ -86,11 +87,25 @@ func supportedPruneTransports(joinStr string) string {
|
||||
return strings.Join(res, joinStr)
|
||||
}
|
||||
|
||||
type pruneUserOptions struct {
|
||||
SkipSummary bool
|
||||
NonInteractive bool
|
||||
}
|
||||
|
||||
func pruneFlags() (pflag.FlagSet, *pruneUserOptions) {
|
||||
opts := pruneUserOptions{}
|
||||
fs := pflag.FlagSet{}
|
||||
fs.BoolVarP(&opts.SkipSummary, "skip-summary", "s", false, "Skip computing the prune summary of freed storage space")
|
||||
fs.BoolVarP(&opts.NonInteractive, "non-interactive", "y", false, "Do not display an interactive prompt for the user to confirm before beginning puning")
|
||||
return fs, &opts
|
||||
}
|
||||
|
||||
type pruneOptions struct {
|
||||
global *globalOptions
|
||||
image *imageOptions
|
||||
retryOpts *retry.Options
|
||||
filterOpts *tagFilterOptions
|
||||
pruneOpts *pruneUserOptions
|
||||
}
|
||||
|
||||
func (p *pruneOptions) intoTagsOptions() *tagsOptions {
|
||||
@ -108,12 +123,14 @@ func pruneCmd(global *globalOptions) *cobra.Command {
|
||||
imageFlags, imageOpts := dockerImageFlags(global, sharedOpts, nil, "", "")
|
||||
retryFlags, retryOpts := retryFlags()
|
||||
filterFlags, filterOpts := filterFlags()
|
||||
pruneFlags, pruneOpts := pruneFlags()
|
||||
|
||||
opts := pruneOptions{
|
||||
global: global,
|
||||
image: imageOpts,
|
||||
retryOpts: retryOpts,
|
||||
filterOpts: filterOpts,
|
||||
pruneOpts: pruneOpts,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -135,6 +152,7 @@ See skopeo-prune(1) section "REPOSITORY NAMES" for the expected format
|
||||
flags.AddFlagSet(&imageFlags)
|
||||
flags.AddFlagSet(&retryFlags)
|
||||
flags.AddFlagSet(&filterFlags)
|
||||
flags.AddFlagSet(&pruneFlags)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -143,7 +161,7 @@ func displayPrunePrompt() bool {
|
||||
// Prompt the user whether they would like to proceed to prune
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
for {
|
||||
fmt.Println("\nwarning: continuing to prune will lead to irreversible data loss")
|
||||
fmt.Println("warning: continuing to prune will lead to irreversible data loss")
|
||||
fmt.Print("continue to prune? (y/n): ")
|
||||
input, _ := reader.ReadString('\n')
|
||||
input = strings.TrimSpace(input)
|
||||
@ -312,7 +330,7 @@ func getSizeMaps(ctx context.Context, sys *types.SystemContext, url string) (map
|
||||
}
|
||||
|
||||
// Function that computes the deduplicated size of a slice of image tags
|
||||
func getDeduplicatedSizeParallel(ctx context.Context, sys *types.SystemContext, opts *tagsOptions, repositoryName string, tags []string) int64 {
|
||||
func getDeduplicatedSizeParallel(ctx context.Context, sys *types.SystemContext, repositoryName string, tags []string) int64 {
|
||||
// Get the config & layer size maps
|
||||
dedupConfigSizeMap := make(map[string]int64)
|
||||
dedupLayerSizeMap := make(map[string]int64)
|
||||
@ -428,7 +446,7 @@ func getDeduplicatedSizeParallel(ctx context.Context, sys *types.SystemContext,
|
||||
|
||||
// Function that displays the prune summary of size freed in pruning
|
||||
// TODO: Determine if errors occurred during size calculation
|
||||
func displayPruneSummary(ctx context.Context, sys *types.SystemContext, opts *tagsOptions, userInput string, toPrune []string, toKeep []string) error {
|
||||
func displayPruneSummary(ctx context.Context, sys *types.SystemContext, userInput string, toPrune []string, toKeep []string) error {
|
||||
imgRef, err := parseDockerRepositoryReference(userInput)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error parsing image reference: %w", err)
|
||||
@ -436,8 +454,8 @@ func displayPruneSummary(ctx context.Context, sys *types.SystemContext, opts *ta
|
||||
repositoryName := imgRef.DockerReference().Name()
|
||||
|
||||
// Compute the deduplicated size of the images that will be pruned vs kept
|
||||
pruneSize := getDeduplicatedSizeParallel(ctx, sys, opts, repositoryName, toPrune)
|
||||
keepSize := getDeduplicatedSizeParallel(ctx, sys, opts, repositoryName, toKeep)
|
||||
pruneSize := getDeduplicatedSizeParallel(ctx, sys, repositoryName, toPrune)
|
||||
keepSize := getDeduplicatedSizeParallel(ctx, sys, repositoryName, toKeep)
|
||||
|
||||
// Summarize the list
|
||||
fmt.Println("")
|
||||
@ -447,12 +465,13 @@ func displayPruneSummary(ctx context.Context, sys *types.SystemContext, opts *ta
|
||||
keepDisp := fmt.Sprintf("Keep\t%d\t%s", len(toKeep), bytesToByteUnit(keepSize))
|
||||
fmt.Fprintln(w, pruneDisp)
|
||||
fmt.Fprintln(w, keepDisp)
|
||||
fmt.Fprintln(w, "")
|
||||
w.Flush()
|
||||
return nil // Success
|
||||
}
|
||||
|
||||
// Function that prunes the tags identified for pruning and displays progress
|
||||
func pruneDockerTagsParallel(ctx context.Context, sys *types.SystemContext, opts *tagsOptions, repositoryName string, tags []string) error {
|
||||
func pruneDockerTagsParallel(ctx context.Context, sys *types.SystemContext, repositoryName string, tags []string) error {
|
||||
// Variable tracking how many tags have been pruned
|
||||
totalTags := len(tags)
|
||||
tagsPruned := 0
|
||||
@ -577,9 +596,9 @@ func getFilteredDockerTags(ctx context.Context, sys *types.SystemContext, opts *
|
||||
}
|
||||
|
||||
// Function that prunes docker tags
|
||||
func pruneDockerTags(ctx context.Context, sys *types.SystemContext, opts *tagsOptions, userInput string) error {
|
||||
func pruneDockerTags(ctx context.Context, sys *types.SystemContext, opts *pruneOptions, userInput string) error {
|
||||
// Get the filtered docker tags for the given repository
|
||||
filteredTags, err := getFilteredDockerTags(ctx, sys, opts, userInput)
|
||||
filteredTags, err := getFilteredDockerTags(ctx, sys, opts.intoTagsOptions(), userInput)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error getting filtered docker tags: %w", err)
|
||||
}
|
||||
@ -597,19 +616,23 @@ func pruneDockerTags(ctx context.Context, sys *types.SystemContext, opts *tagsOp
|
||||
|
||||
// Display the prune summary
|
||||
// TODO: Error check - determine if errors occurred during size calculation
|
||||
err = displayPruneSummary(ctx, sys, opts, userInput, toPrune, toKeep)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error displaying prune summary: %w", err)
|
||||
if !opts.pruneOpts.SkipSummary {
|
||||
err = displayPruneSummary(ctx, sys, userInput, toPrune, toKeep)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error displaying prune summary: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Display the prune prompt
|
||||
accepted := displayPrunePrompt()
|
||||
if !accepted{
|
||||
return nil // User decided not to prune
|
||||
if !opts.pruneOpts.NonInteractive {
|
||||
accepted := displayPrunePrompt()
|
||||
if !accepted{
|
||||
return nil // User decided not to prune
|
||||
}
|
||||
}
|
||||
|
||||
// Prune the docker tags
|
||||
err = pruneDockerTagsParallel(ctx, sys, opts, userInput, toPrune)
|
||||
err = pruneDockerTagsParallel(ctx, sys, userInput, toPrune)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error pruning tags: %w", err)
|
||||
}
|
||||
@ -635,7 +658,7 @@ func (opts *pruneOptions) run(args []string, stdout io.Writer) (retErr error) {
|
||||
}
|
||||
|
||||
if val, ok := pruneTransportHandlers[transport.Name()]; ok {
|
||||
err = val(ctx, sys, opts.intoTagsOptions(), args[0])
|
||||
err = val(ctx, sys, opts, args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -646,5 +669,3 @@ func (opts *pruneOptions) run(args []string, stdout io.Writer) (retErr error) {
|
||||
|
||||
return nil // Success
|
||||
}
|
||||
|
||||
// TODO: Prune-many command
|
||||
|
Loading…
Reference in New Issue
Block a user