Skip to content

Commit

Permalink
Merge pull request #12 from deexithparand/issue-3-image-size-threshold
Browse files Browse the repository at this point in the history
Completed Feature: Image Size Threshold (Issue : #3)
  • Loading branch information
mmuazam98 authored Oct 28, 2024
2 parents 534a6a6 + ad17464 commit be5cf6c
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 53 deletions.
6 changes: 6 additions & 0 deletions cmd/cleaner/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
6 changes: 4 additions & 2 deletions internal/docker/containers.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
)

// CleanupStoppedContainerImages removes images associated with stopped containers
func (d *DockerClient) CleanupStoppedContainerImages() {
func (d *DockerClient) CleanupStoppedContainerImages() error {

const (
ImageStateUnreferenced = -1
Expand All @@ -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)
Expand Down Expand Up @@ -81,4 +82,5 @@ func (d *DockerClient) CleanupStoppedContainerImages() {
}
}

return nil
}
109 changes: 59 additions & 50 deletions internal/docker/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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}
Expand All @@ -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
}
59 changes: 59 additions & 0 deletions internal/docker/utils.go
Original file line number Diff line number Diff line change
@@ -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]
}
34 changes: 33 additions & 1 deletion pkg/utils/flags.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,50 @@
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 {
f := &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
}

0 comments on commit be5cf6c

Please sign in to comment.