diff --git a/cmd/cleaner/main.go b/cmd/cleaner/main.go index 2fea88b..8d264b1 100644 --- a/cmd/cleaner/main.go +++ b/cmd/cleaner/main.go @@ -25,6 +25,12 @@ func main() { dc.CleanupStoppedContainerImages() case f.VerboseMode: dc.VerboseModeCleanup() + case f.SizeLimit.Value >= 0: + var unit string = f.GetSizeUnit() + if unit == "" { + log.Fatalf("Please specify a size unit (B, KB, MB, or GB)") + } + dc.RemoveExceedSizeLimit(f.SizeLimit.Value, unit) default: dc.RemoveUnusedImages() } diff --git a/internal/docker/containers.go b/internal/docker/containers.go index 0e1128f..a7419e8 100644 --- a/internal/docker/containers.go +++ b/internal/docker/containers.go @@ -9,7 +9,7 @@ import ( ) // CleanupStoppedContainerImages removes images associated with stopped containers -func (d *DockerClient) CleanupStoppedContainerImages() { +func (d *DockerClient) CleanupStoppedContainerImages() error { const ( ImageStateUnreferenced = -1 @@ -20,7 +20,8 @@ func (d *DockerClient) CleanupStoppedContainerImages() { // List all images images, err := d.CLI.ImageList(context.Background(), image.ListOptions{}) if err != nil { - log.Fatalf("Error listing images: %v", err) + log.Printf("Error listing images: %v", err) + return err } imagesForCleanup := make(map[string]int8) @@ -81,4 +82,5 @@ func (d *DockerClient) CleanupStoppedContainerImages() { } } + return nil } diff --git a/internal/docker/images.go b/internal/docker/images.go index b1dc5fe..f0c6f05 100644 --- a/internal/docker/images.go +++ b/internal/docker/images.go @@ -26,44 +26,45 @@ func (d *DockerClient) ListUnusedImages() ([]image.Summary, error) { } } - if len(unusedImages) > 0 { - fmt.Printf("Found %d unused images\n", len(unusedImages)) - } return unusedImages, nil } // PrintUnusedImages lists the images that would be removed (Dry Run) -func (d *DockerClient) PrintUnusedImages() { +func (d *DockerClient) PrintUnusedImages() error { images, err := d.ListUnusedImages() if err != nil { - log.Fatalf("Error listing images: %v", err) + log.Printf("Error listing images: %v", err) + return err } if len(images) == 0 { - fmt.Println("No unused images found.") - return + log.Printf("No unused images found.") + return nil } - fmt.Println("The following images would be removed:") + log.Println("The following images would be removed:") for _, image := range images { - fmt.Printf("ID: %s, Created: %d\n", image.ID, image.Created) + log.Printf("ID: %s, Created: %d\n", image.ID, image.Created) } + + return nil } // VerboseModeCleanup gives more details while doing the cleanup of unused images -func (d *DockerClient) VerboseModeCleanup() { +func (d *DockerClient) VerboseModeCleanup() error { images, err := d.ListUnusedImages() if err != nil { - log.Fatalf("Error listing images: %v", err) + log.Printf("Error listing images: %v", err) + return err } opts := image.RemoveOptions{Force: true} if len(images) == 0 { log.Println("No unused images found") - return + return nil } log.Printf("Found %d unused images. Starting removal in verbose mode...\n", len(images)) @@ -87,68 +88,75 @@ func (d *DockerClient) VerboseModeCleanup() { } else { // timestamp in RFC3339 format created := time.Unix(image.Created, 0).Format(time.RFC3339) - truncatedDockerImageID := truncateDockerImageID(image.ID, 32) // Print image information in a table-like format fmt.Printf(tableformat, - truncatedDockerImageID, - formatSize(image.Size), + FormatDockerImageID(image.ID, 32), + FormatSize(image.Size), created, "Removed", - formatLabels(image.Labels), + FormatLabels(image.Labels), ) } } + return nil } -// Helper function to truncate strings with ellipsis -func truncateDockerImageID(s string, maxLen int) string { - if len(s) <= maxLen { - return s - } - return s[:maxLen-3] + "..." -} +// Remove Images that exceed a specific size limit +func (d *DockerClient) RemoveExceedSizeLimit(sizeLimit float64, unit string) error { -// Helper function to converts bytes to human-readable format -func formatSize(bytes int64) string { - const unit = 1024 - if bytes < unit { - return fmt.Sprintf("%d B", bytes) - } - div, exp := int64(unit), 0 - for n := bytes / unit; n >= unit; n /= unit { - div *= unit - exp++ + var sizeLimitInBytes int64 = int64(ToBytes(sizeLimit, unit)) + + images, err := d.ListUnusedImages() + if err != nil { + log.Printf("Error listing images: %v", err) + return err } - return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp]) -} -// Helper function used to format the docker image labels Key Value Pairs -func formatLabels(labels map[string]string) string { - if len(labels) == 0 { - return "No Labels Found" + if len(images) == 0 { + log.Printf("No unused images found") + return nil } - var labelStore []string - for labelKey, labelValue := range labels { - // Handling empty values - if labelValue == "" { - labelStore = append(labelStore, labelKey) - continue + opts := image.RemoveOptions{Force: true} + + removedImagesCount := 0 + totalSizeCleaned := int64(0) + + for _, image := range images { + + // checking and removing images exceeding the threshold size + if image.Size > sizeLimitInBytes { + _, err := d.CLI.ImageRemove(context.Background(), image.ID, opts) + if err != nil { + log.Printf("Failed to remove image %s: %v", image.ID, err) + } else { + log.Printf("Successfully removed image %s", image.ID) + } + + totalSizeCleaned += image.Size + removedImagesCount++ } - labelStore = append(labelStore, fmt.Sprintf("%s:%s", labelKey, labelValue)) + + } + + if removedImagesCount > 0 { + log.Printf("Summary: Removed %d images (Total space freed: %s)", removedImagesCount, FormatSize(totalSizeCleaned)) + } else { + log.Printf("No Unused Images are exceeding the limit %d %s", int64(sizeLimit), strings.ToUpper(unit)) } - return strings.Join(labelStore, ", ") + return nil } // RemoveUnusedImages deletes unused Docker images -func (d *DockerClient) RemoveUnusedImages() { +func (d *DockerClient) RemoveUnusedImages() error { images, err := d.ListUnusedImages() if err != nil { - log.Fatalf("Error listing images: %v", err) + log.Printf("Error listing images: %v", err) + return err } opts := image.RemoveOptions{Force: true} @@ -159,7 +167,8 @@ func (d *DockerClient) RemoveUnusedImages() { log.Printf("Failed to remove image %s: %v", image.ID, err) } else { log.Printf("Successfully removed image %s", image.ID) - } } + + return nil } diff --git a/internal/docker/utils.go b/internal/docker/utils.go new file mode 100644 index 0000000..2d9891b --- /dev/null +++ b/internal/docker/utils.go @@ -0,0 +1,59 @@ +package docker + +import ( + "fmt" + "strings" +) + +// Helper function to truncate docker image ID with ellipsis +func FormatDockerImageID(s string, maxLen int) string { + if len(s) <= maxLen { + return s + } + return s[:maxLen-3] + "..." +} + +// Helper function to converts bytes to human-readable format +func FormatSize(bytes int64) string { + const unit = 1024.0 + if bytes < unit { + return fmt.Sprintf("%d B", bytes) + } + div, exp := int64(unit), 0 + for n := bytes / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp]) +} + +// Helper function used to format the docker image labels Key Value Pairs +func FormatLabels(labels map[string]string) string { + if len(labels) == 0 { + return "No Labels Found" + } + + var labelStore []string + for labelKey, labelValue := range labels { + // Handling empty values + if labelValue == "" { + labelStore = append(labelStore, labelKey) + continue + } + labelStore = append(labelStore, fmt.Sprintf("%s:%s", labelKey, labelValue)) + } + + return strings.Join(labelStore, ", ") +} + +// converts value to bytes +func ToBytes(size float64, unit string) float64 { + multipliers := map[string]float64{ + "B": 1.0, + "KB": 1024.0, + "MB": 1024.0 * 1024.0, + "GB": 1024.0 * 1024.0 * 1024.0, + } + + return size * multipliers[unit] +} diff --git a/pkg/utils/flags.go b/pkg/utils/flags.go index 998f814..819b5c7 100644 --- a/pkg/utils/flags.go +++ b/pkg/utils/flags.go @@ -1,11 +1,37 @@ package utils -import "flag" +import ( + "flag" +) + +type Size struct { + Value float64 + Unit string +} type Flags struct { DryRun bool RemoveStopped bool VerboseMode bool + SizeLimit Size + B bool + KB bool + MB bool + GB bool +} + +func (f *Flags) GetSizeUnit() string { + if f.B { + return "B" + } else if f.KB { + return "KB" + } else if f.MB { + return "MB" + } else if f.GB { + return "GB" + } else { + return "" + } } func ParseFlags() *Flags { @@ -13,6 +39,12 @@ func ParseFlags() *Flags { flag.BoolVar(&f.DryRun, "dry-run", false, "List unused Docker images without deleting them") flag.BoolVar(&f.RemoveStopped, "remove-stopped", false, "Remove Images Associated with Stopped Containers") flag.BoolVar(&f.VerboseMode, "verbose", false, "Verbose mode provides additional details about each image during cleanup") + flag.Float64Var(&f.SizeLimit.Value, "size-limit", 0, "Specify the size limit to filter images (e.g., 500MB, 1GB)") + flag.BoolVar(&f.B, "B", false, "Specify the size unit as bytes") + flag.BoolVar(&f.KB, "KB", false, "Specify the size unit as kilobytes") + flag.BoolVar(&f.MB, "MB", false, "Specify the size unit as megabytes") + flag.BoolVar(&f.GB, "GB", false, "Specify the size unit as gigabytes") + flag.Parse() return f }