diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bcfc5c6..3ebf30f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -44,3 +44,7 @@ jobs: - name: build run: make binaries working-directory: . + + - name: test + run: make unit-tests + working-directory: . \ No newline at end of file diff --git a/.gitignore b/.gitignore index 39e3456..3be10e1 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /skiff +bin/ +vendor/ diff --git a/Makefile b/Makefile index 0da9af7..a8c114b 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,18 @@ -.PHONY: binaries +.PHONY: binaries vendor unit-tests behave # Build the binaries binaries: go build -o bin/skiff ./cmd/skiff + +# Run unit tests +unit-tests: + go test -v ./cmd/skiff/... + +# Run behave tests +behave: + behave features/ + +vendor: + go mod tidy + go mod vendor + go mod verify diff --git a/cmd/skiff/main.go b/cmd/skiff/main.go index 396e92c..8d8ebe0 100644 --- a/cmd/skiff/main.go +++ b/cmd/skiff/main.go @@ -13,7 +13,6 @@ import ( ) func main() { - cmd := &cli.Command{ Name: "skiff", Usage: "Analyze the disk usage and directory structure of OCI images and its layers", diff --git a/cmd/skiff/top.go b/cmd/skiff/top.go index bd83525..9496a62 100644 --- a/cmd/skiff/top.go +++ b/cmd/skiff/top.go @@ -6,25 +6,38 @@ import ( "context" "fmt" "io" + "os" "path/filepath" "slices" "strconv" "strings" + "text/tabwriter" "github.com/containers/image/v5/pkg/blobinfocache/none" "github.com/containers/image/v5/pkg/compression" "github.com/containers/image/v5/types" + "github.com/opencontainers/go-digest" "github.com/urfave/cli/v3" skiff "github.com/dcermak/skiff/pkg" ) var topCommand = cli.Command{ - Name: "top", - Usage: "Analyze a container image and list files by size", + Name: "top", + Usage: "Analyze a container image and list files by size", + ArgsUsage: "[image]", Flags: []cli.Flag{ &cli.BoolFlag{Name: "include-pseudo", Usage: "Include pseudo-filesystems (/dev, /proc, /sys)"}, &cli.BoolFlag{Name: "follow-symlinks", Usage: "Follow symbolic links"}, + &cli.BoolFlag{ + Name: "human-readable", + Usage: "Show file sizes in human readable format", + }, + &cli.StringSliceFlag{ + Name: "layer", + Usage: "Filter results to specific layer(s) by diffID (uncompressed SHA256). If not specified, all layers are included (not an empty result).", + Aliases: []string{"l", "diff-id"}, + }, }, Arguments: []cli.Argument{ &cli.StringArg{Name: "image", UsageText: "Container image ref"}, @@ -35,17 +48,25 @@ var topCommand = cli.Command{ return fmt.Errorf("image URL is required") } + humanReadable := c.Bool("human-readable") + layers := c.StringSlice("layer") + if c.IsSet("layer") && len(layers) == 0 { + return fmt.Errorf("--layer flag provided but no diffID specified; please provide at least one diffID") + } + sysCtx := types.SystemContext{} - return analyzeLayers(image, ctx, &sysCtx) + + return analyzeLayers(ctx, &sysCtx, image, layers, humanReadable) }, } const defaultFileLimit = 10 type FileInfo struct { - Path string - Size int64 - Layer string // layer ID this file belongs to + Path string + Size int64 + HumanReadableSize string + DiffID digest.Digest // diffID of the layer this file belongs to } type FileHeap []FileInfo @@ -66,31 +87,85 @@ func (h *FileHeap) Pop() interface{} { return item } +// getLayersByDiffID returns layer blob infos filtered by user-provided diffIDs +// by looking up diffIDs and mapping to manifest layers +func getLayersByDiffID(manifestLayers []types.BlobInfo, allDiffIDs []digest.Digest, filterDiffIDs []string) ([]types.BlobInfo, []digest.Digest, error) { + // If no filtering, return all layers with all their diffIDs + if len(filterDiffIDs) == 0 { + return manifestLayers, allDiffIDs, nil + } + + // Filter layers by user-provided diffIDs + var filteredLayers []types.BlobInfo + var filteredDiffIDs []digest.Digest + + // Map user diffIDs to layer indices + for _, userDiffID := range filterDiffIDs { + found := false + for i, configDiffID := range allDiffIDs { + // Match full diffID or prefix + if configDiffID.String() == userDiffID || strings.HasPrefix(configDiffID.Encoded(), userDiffID) { + if i < len(manifestLayers) { + filteredLayers = append(filteredLayers, manifestLayers[i]) + filteredDiffIDs = append(filteredDiffIDs, configDiffID) + found = true + break + } + } + } + if !found { + return nil, nil, fmt.Errorf("diffID %s not found in image", userDiffID) + } + } + + return filteredLayers, filteredDiffIDs, nil +} + // analyzeLayers fetches layers for a given image reference // reads the associated layer archives and lists file info -func analyzeLayers(uri string, ctx context.Context, sysCtx *types.SystemContext) error { +func analyzeLayers(ctx context.Context, sysCtx *types.SystemContext, uri string, layers []string, humanReadable bool) error { + // represents an image from any transport (docker://, containers-storage://, etc.) img, _, err := skiff.ImageAndLayersFromURI(ctx, sysCtx, uri) if err != nil { return err } + // image source that helps us fetch layers to eventually show files from the stream imgSrc, err := img.Reference().NewImageSource(ctx, sysCtx) if err != nil { return err } defer imgSrc.Close() - h := &FileHeap{} - heap.Init(h) + // Get transport-specific layer blob infos + manifestLayers, err := skiff.BlobInfoFromImage(ctx, sysCtx, img) + if err != nil { + return fmt.Errorf("failed to get blob info from image: %w", err) + } + + conf, err := img.OCIConfig(ctx) + allDiffIDs := []digest.Digest{} - files := make([]FileInfo, h.Len()) + // only get them if the rootfs type is correct + if err == nil && conf != nil && conf.RootFS.Type == "layers" { + allDiffIDs = conf.RootFS.DiffIDs + } + + // Check that manifestLayers and allDiffIDs have matching lengths + if len(manifestLayers) != len(allDiffIDs) { + return fmt.Errorf("manifestLayers (%d) and allDiffIDs (%d) length mismatch", len(manifestLayers), len(allDiffIDs)) + } - layerInfos, err := skiff.BlobInfoFromImage(img, ctx, sysCtx) + // Get filtered layers and their diffIDs + layerInfos, diffIDs, err := getLayersByDiffID(manifestLayers, allDiffIDs, layers) if err != nil { return err } - for _, layer := range layerInfos { + h := &FileHeap{} + heap.Init(h) + + for i, layer := range layerInfos { blob, _, err := imgSrc.GetBlob(context.Background(), layer, none.NoCache) if err != nil { return err @@ -103,6 +178,9 @@ func analyzeLayers(uri string, ctx context.Context, sysCtx *types.SystemContext) } defer uncompressedStream.Close() + // Get the diffID for this layer + layerDiffID := diffIDs[i] + tr := tar.NewReader(uncompressedStream) for { hdr, err := tr.Next() @@ -113,48 +191,58 @@ func analyzeLayers(uri string, ctx context.Context, sysCtx *types.SystemContext) return fmt.Errorf("failed to read tar header: %w", err) } - // TODO: follow symlinks + // TODO(danishprakash): follow symlinks // if hdr.Typeflag == tar.TypeSymlink path, err := filepath.Abs(filepath.Join("/", hdr.Name)) if err != nil { - // TODO: perhaps just log and not error out - return fmt.Errorf("error generating absolute representation of path: %w", err) + // Log the error but continue processing other files + fmt.Fprintf(os.Stderr, "warning: error generating absolute representation of path %s: %v\n", hdr.Name, err) + continue } if hdr.Typeflag == tar.TypeReg { - heap.Push(h, FileInfo{Path: path, Size: hdr.Size, Layer: layer.Digest.Encoded()}) + fileInfo := FileInfo{ + Path: path, + Size: hdr.Size, + DiffID: layerDiffID, + } + if humanReadable { + fileInfo.HumanReadableSize = skiff.HumanReadableSize(hdr.Size) + } + heap.Push(h, fileInfo) if h.Len() > defaultFileLimit { heap.Pop(h) } } } - } - for i := 0; h.Len() > 0; i++ { + // Extract files from heap in reverse order (largest first) + var files []FileInfo + for h.Len() > 0 { files = append(files, heap.Pop(h).(FileInfo)) } - maxPathLen, maxSizeLen, maxLayerLen := 0, 0, 12 - for _, f := range files { - if len(f.Path) > maxPathLen { - maxPathLen = len(f.Path) - } - sizeStr := strconv.FormatInt(f.Size, 10) - if len(sizeStr) > maxSizeLen { - maxSizeLen = len(sizeStr) - } - } - - fmt.Printf("%-*s %*s %s\n", maxPathLen, "File Path", maxSizeLen, "Size", "Layer ID") - fmt.Println(strings.Repeat("-", maxPathLen+maxSizeLen+maxLayerLen+15)) // also consider two tab chars + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', tabwriter.TabIndent) + defer w.Flush() + fmt.Fprintln(w, "FILE PATH\tSIZE\tDIFF ID") slices.Reverse(files) for _, f := range files { - sizeStr := strconv.FormatInt(f.Size, 10) - fmt.Printf("%-*s %*s %-*s\n", maxPathLen, f.Path, maxSizeLen, sizeStr, maxLayerLen, f.Layer[:12]) + var size string + if humanReadable { + size = f.HumanReadableSize + } else { + size = strconv.FormatInt(f.Size, 10) + } + // Show first 12 chars of diffID (digest.Digest.Encoded() gives us just the hex part) + // TODO(dcermak) switch to skiff.FormatDigest(f.DiffID, false) + diffIDDisplay := f.DiffID.Encoded() + if len(diffIDDisplay) > 12 { + diffIDDisplay = diffIDDisplay[:12] + } + fmt.Fprintf(w, "%s\t%s\t%s\n", f.Path, size, diffIDDisplay) } - return nil } diff --git a/cmd/skiff/top_test.go b/cmd/skiff/top_test.go new file mode 100644 index 0000000..5b765f8 --- /dev/null +++ b/cmd/skiff/top_test.go @@ -0,0 +1,181 @@ +package main + +import ( + "strings" + "testing" + + "container/heap" + + "github.com/containers/image/v5/types" + "github.com/opencontainers/go-digest" + + skiff "github.com/dcermak/skiff/pkg" +) + +func TestHumanReadableSize(t *testing.T) { + tests := []struct { + name string + bytes int64 + expected string + }{ + {"zero bytes", 0, "0 B"}, + {"small bytes", 500, "500 B"}, + {"1 KB", 1000, "1.0 kB"}, + {"1.5 KB", 1500, "1.5 kB"}, + {"1 MB", 1000000, "1.0 MB"}, + {"1.5 MB", 1500000, "1.5 MB"}, + {"1 GB", 1000000000, "1.0 GB"}, + {"1.5 GB", 1500000000, "1.5 GB"}, + {"large number", 1234567890, "1.2 GB"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := skiff.HumanReadableSize(tt.bytes) + if result != tt.expected { + t.Errorf("HumanReadableSize(%d) = %s, want %s", tt.bytes, result, tt.expected) + } + }) + } +} + +func TestFileHeap(t *testing.T) { + h := &FileHeap{} + heap.Init(h) + + // Test empty heap + if h.Len() != 0 { + t.Errorf("Expected empty heap to have length 0, got %d", h.Len()) + } + + // Test pushing items + items := []FileInfo{ + {Path: "/file1", Size: 100}, + {Path: "/file2", Size: 50}, + {Path: "/file3", Size: 200}, + } + + for _, item := range items { + heap.Push(h, item) + } + + if h.Len() != 3 { + t.Errorf("Expected heap to have length 3, got %d", h.Len()) + } + + // Test that smallest item is at the top (min heap) + smallest := heap.Pop(h).(FileInfo) + if smallest.Size != 50 { + t.Errorf("Expected smallest item to have size 50, got %d", smallest.Size) + } + + // Test remaining items + if h.Len() != 2 { + t.Errorf("Expected heap to have length 2 after pop, got %d", h.Len()) + } + + next := heap.Pop(h).(FileInfo) + if next.Size != 100 { + t.Errorf("Expected next item to have size 100, got %d", next.Size) + } + + last := heap.Pop(h).(FileInfo) + if last.Size != 200 { + t.Errorf("Expected last item to have size 200, got %d", last.Size) + } + + if h.Len() != 0 { + t.Errorf("Expected empty heap after all pops, got length %d", h.Len()) + } +} + + +func TestGetLayersByDiffID(t *testing.T) { + // Create test layers + layer1 := types.BlobInfo{Digest: digest.Digest("sha256:layer1digest")} + layer2 := types.BlobInfo{Digest: digest.Digest("sha256:layer2digest")} + layer3 := types.BlobInfo{Digest: digest.Digest("sha256:layer3digest")} + manifestLayers := []types.BlobInfo{layer1, layer2, layer3} + + // Create test diffIDs + diffID1 := digest.Digest("sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef") + diffID2 := digest.Digest("sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890") + diffID3 := digest.Digest("sha256:fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321") + allDiffIDs := []digest.Digest{diffID1, diffID2, diffID3} + + tests := []struct { + name string + filterDiffIDs []string + expectedCount int + expectError bool + errorContains string + }{ + { + name: "no filters - return all layers", + filterDiffIDs: []string{}, + expectedCount: 3, + expectError: false, + }, + { + name: "filter by full diffID", + filterDiffIDs: []string{"sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"}, + expectedCount: 1, + expectError: false, + }, + { + name: "filter by partial diffID", + filterDiffIDs: []string{"1234567890abcdef"}, + expectedCount: 1, + expectError: false, + }, + { + name: "filter by non-existent diffID", + filterDiffIDs: []string{"nonexistent"}, + expectedCount: 0, + expectError: true, + errorContains: "diffID nonexistent not found", + }, + { + name: "filter by multiple diffIDs", + filterDiffIDs: []string{"1234567890abcdef", "abcdef1234567890"}, + expectedCount: 2, + expectError: false, + }, + { + name: "filter by partial diffID - second layer", + filterDiffIDs: []string{"abcdef1234"}, + expectedCount: 1, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + layers, diffIDs, err := getLayersByDiffID(manifestLayers, allDiffIDs, tt.filterDiffIDs) + + if tt.expectError { + if err == nil { + t.Errorf("Expected error but got none") + return + } + if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) { + t.Errorf("Expected error to contain '%s', got '%s'", tt.errorContains, err.Error()) + } + return + } + + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + + if len(layers) != tt.expectedCount { + t.Errorf("Expected %d layers, got %d", tt.expectedCount, len(layers)) + } + + if len(diffIDs) != tt.expectedCount { + t.Errorf("Expected %d diffIDs, got %d", tt.expectedCount, len(diffIDs)) + } + }) + } +} diff --git a/features/top.feature b/features/top.feature index 94204c8..a58fd25 100644 --- a/features/top.feature +++ b/features/top.feature @@ -14,18 +14,17 @@ Feature: `skiff top` command Then the exit code is 0 And stdout is """ - File Path Size Layer ID - ------------------------------------------------------------------- - /usr/bin/container-suseconnect 9245304 abb83fe2605d - /usr/lib64/libzypp.so.1735.1.1 8767504 abb83fe2605d - /usr/lib/sysimage/rpm/Packages.db 7837536 dbdff6b3e297 - /usr/lib64/libpython3.11.so.1.0 5876440 dbdff6b3e297 - /usr/lib64/libcrypto.so.3.1.4 5715672 abb83fe2605d - /usr/lib/sysimage/rpm/Packages.db 5190128 abb83fe2605d - /usr/share/misc/magic.mgc 4983184 abb83fe2605d - /usr/lib/git/git 3726520 dbdff6b3e297 - /usr/lib/locale/locale-archive 3058640 abb83fe2605d - /usr/bin/zypper 2915456 abb83fe2605d + FILE PATH SIZE DIFF ID + /usr/bin/container-suseconnect 9245304 4672d0cba723 + /usr/lib64/libzypp.so.1735.1.1 8767504 4672d0cba723 + /usr/lib/sysimage/rpm/Packages.db 7837536 88304527ded0 + /usr/lib64/libpython3.11.so.1.0 5876440 88304527ded0 + /usr/lib64/libcrypto.so.3.1.4 5715672 4672d0cba723 + /usr/lib/sysimage/rpm/Packages.db 5190128 4672d0cba723 + /usr/share/misc/magic.mgc 4983184 4672d0cba723 + /usr/lib/git/git 3726520 88304527ded0 + /usr/lib/locale/locale-archive 3058640 4672d0cba723 + /usr/bin/zypper 2915456 4672d0cba723 """ Scenario: Analyze an image from containers-storage with top command @@ -35,16 +34,113 @@ Feature: `skiff top` command Then the exit code is 0 And stdout is """ - File Path Size Layer ID - ------------------------------------------------------------------- - /usr/bin/container-suseconnect 9245304 4672d0cba723 - /usr/lib64/libzypp.so.1735.1.1 8767504 4672d0cba723 - /usr/lib/sysimage/rpm/Packages.db 7837536 88304527ded0 - /usr/lib64/libpython3.11.so.1.0 5876440 88304527ded0 - /usr/lib64/libcrypto.so.3.1.4 5715672 4672d0cba723 - /usr/lib/sysimage/rpm/Packages.db 5190128 4672d0cba723 - /usr/share/misc/magic.mgc 4983184 4672d0cba723 - /usr/lib/git/git 3726520 88304527ded0 - /usr/lib/locale/locale-archive 3058640 4672d0cba723 - /usr/bin/zypper 2915456 4672d0cba723 + FILE PATH SIZE DIFF ID + /usr/bin/container-suseconnect 9245304 4672d0cba723 + /usr/lib64/libzypp.so.1735.1.1 8767504 4672d0cba723 + /usr/lib/sysimage/rpm/Packages.db 7837536 88304527ded0 + /usr/lib64/libpython3.11.so.1.0 5876440 88304527ded0 + /usr/lib64/libcrypto.so.3.1.4 5715672 4672d0cba723 + /usr/lib/sysimage/rpm/Packages.db 5190128 4672d0cba723 + /usr/share/misc/magic.mgc 4983184 4672d0cba723 + /usr/lib/git/git 3726520 88304527ded0 + /usr/lib/locale/locale-archive 3058640 4672d0cba723 + /usr/bin/zypper 2915456 4672d0cba723 + """ + + Scenario: Filter by single layer using partial digest + Given I run skiff with the subcommand "top --layer 4672d0 registry.suse.com/bci/python@sha256:677b52cc1d587ff72430f1b607343a3d1f88b15a9bbd999601554ff303d6774f" + Then the exit code is 0 + And stdout is + """ + FILE PATH SIZE DIFF ID + /usr/bin/container-suseconnect 9245304 4672d0cba723 + /usr/lib64/libzypp.so.1735.1.1 8767504 4672d0cba723 + /usr/lib64/libcrypto.so.3.1.4 5715672 4672d0cba723 + /usr/lib/sysimage/rpm/Packages.db 5190128 4672d0cba723 + /usr/share/misc/magic.mgc 4983184 4672d0cba723 + /usr/lib/locale/locale-archive 3058640 4672d0cba723 + /usr/bin/zypper 2915456 4672d0cba723 + /lib64/libc.so.6 2449832 4672d0cba723 + /usr/lib64/libstdc++.so.6.0.33 2424040 4672d0cba723 + /usr/lib64/ossl-modules/fips.so 2285504 4672d0cba723 + """ + + Scenario: Filter by multiple layers + Given I run skiff with the subcommand "top --layer 4672d0cba723 --layer 88304527ded0 registry.suse.com/bci/python@sha256:677b52cc1d587ff72430f1b607343a3d1f88b15a9bbd999601554ff303d6774f" + Then the exit code is 0 + And stdout is + """ + FILE PATH SIZE DIFF ID + /usr/bin/container-suseconnect 9245304 4672d0cba723 + /usr/lib64/libzypp.so.1735.1.1 8767504 4672d0cba723 + /usr/lib/sysimage/rpm/Packages.db 7837536 88304527ded0 + /usr/lib64/libpython3.11.so.1.0 5876440 88304527ded0 + /usr/lib64/libcrypto.so.3.1.4 5715672 4672d0cba723 + /usr/lib/sysimage/rpm/Packages.db 5190128 4672d0cba723 + /usr/share/misc/magic.mgc 4983184 4672d0cba723 + /usr/lib/git/git 3726520 88304527ded0 + /usr/lib/locale/locale-archive 3058640 4672d0cba723 + /usr/bin/zypper 2915456 4672d0cba723 + """ + + Scenario: Filter by non-existent layer + Given I run skiff with the subcommand "top --layer nonexistentlayer registry.suse.com/bci/python@sha256:677b52cc1d587ff72430f1b607343a3d1f88b15a9bbd999601554ff303d6774f" + Then the exit code is 1 + And stderr contains + """ + diffID nonexistentlayer not found in image + """ + + Scenario: Use --human-readable flag for human-readable file sizes + Given I run skiff with the subcommand "top --human-readable registry.suse.com/bci/python@sha256:677b52cc1d587ff72430f1b607343a3d1f88b15a9bbd999601554ff303d6774f" + Then the exit code is 0 + And stdout is + """ + FILE PATH SIZE DIFF ID + /usr/bin/container-suseconnect 9.2 MB 4672d0cba723 + /usr/lib64/libzypp.so.1735.1.1 8.8 MB 4672d0cba723 + /usr/lib/sysimage/rpm/Packages.db 7.8 MB 88304527ded0 + /usr/lib64/libpython3.11.so.1.0 5.9 MB 88304527ded0 + /usr/lib64/libcrypto.so.3.1.4 5.7 MB 4672d0cba723 + /usr/lib/sysimage/rpm/Packages.db 5.2 MB 4672d0cba723 + /usr/share/misc/magic.mgc 5.0 MB 4672d0cba723 + /usr/lib/git/git 3.7 MB 88304527ded0 + /usr/lib/locale/locale-archive 3.1 MB 4672d0cba723 + /usr/bin/zypper 2.9 MB 4672d0cba723 + """ + + Scenario: Use --human-readable with layer filtering + Given I run skiff with the subcommand "top --human-readable --layer 4672d0cba723 registry.suse.com/bci/python@sha256:677b52cc1d587ff72430f1b607343a3d1f88b15a9bbd999601554ff303d6774f" + Then the exit code is 0 + And stdout is + """ + FILE PATH SIZE DIFF ID + /usr/bin/container-suseconnect 9.2 MB 4672d0cba723 + /usr/lib64/libzypp.so.1735.1.1 8.8 MB 4672d0cba723 + /usr/lib64/libcrypto.so.3.1.4 5.7 MB 4672d0cba723 + /usr/lib/sysimage/rpm/Packages.db 5.2 MB 4672d0cba723 + /usr/share/misc/magic.mgc 5.0 MB 4672d0cba723 + /usr/lib/locale/locale-archive 3.1 MB 4672d0cba723 + /usr/bin/zypper 2.9 MB 4672d0cba723 + /lib64/libc.so.6 2.4 MB 4672d0cba723 + /usr/lib64/libstdc++.so.6.0.33 2.4 MB 4672d0cba723 + /usr/lib64/ossl-modules/fips.so 2.3 MB 4672d0cba723 + """ + + Scenario: Use --human-readable with multiple layer filtering + Given I run skiff with the subcommand "top --human-readable --layer 4672d0cba723 --layer 88304527ded0 registry.suse.com/bci/python@sha256:677b52cc1d587ff72430f1b607343a3d1f88b15a9bbd999601554ff303d6774f" + Then the exit code is 0 + And stdout is + """ + FILE PATH SIZE DIFF ID + /usr/bin/container-suseconnect 9.2 MB 4672d0cba723 + /usr/lib64/libzypp.so.1735.1.1 8.8 MB 4672d0cba723 + /usr/lib/sysimage/rpm/Packages.db 7.8 MB 88304527ded0 + /usr/lib64/libpython3.11.so.1.0 5.9 MB 88304527ded0 + /usr/lib64/libcrypto.so.3.1.4 5.7 MB 4672d0cba723 + /usr/lib/sysimage/rpm/Packages.db 5.2 MB 4672d0cba723 + /usr/share/misc/magic.mgc 5.0 MB 4672d0cba723 + /usr/lib/git/git 3.7 MB 88304527ded0 + /usr/lib/locale/locale-archive 3.1 MB 4672d0cba723 + /usr/bin/zypper 2.9 MB 4672d0cba723 """ diff --git a/go.mod b/go.mod index e908908..2b6a86c 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/containers/image/v5 v5.36.1 github.com/containers/storage v1.59.1 github.com/opencontainers/go-digest v1.0.0 + github.com/opencontainers/image-spec v1.1.1 github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 github.com/urfave/cli/v3 v3.4.1 ) @@ -68,7 +69,6 @@ require ( github.com/moby/sys/user v0.4.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/opencontainers/image-spec v1.1.1 // indirect github.com/opencontainers/runc v1.3.0 // indirect github.com/opencontainers/runtime-spec v1.2.1 // indirect github.com/opencontainers/selinux v1.12.0 // indirect diff --git a/pkg/image.go b/pkg/image.go index fa1420d..c739388 100644 --- a/pkg/image.go +++ b/pkg/image.go @@ -75,8 +75,6 @@ func ImageAndLayersFromURI(ctx context.Context, sysCtx *types.SystemContext, uri } img, _, err := runtime.LookupImage(uri, nil) - - // found the image in store => exit if err == nil { ref, err := img.StorageReference() if err != nil { @@ -123,13 +121,12 @@ func ImageAndLayersFromURI(ctx context.Context, sysCtx *types.SystemContext, uri // // Returns a slice of BlobInfo containing layer information needed for // blob retrieval operations. -func BlobInfoFromImage(img types.Image, ctx context.Context, sysCtx *types.SystemContext) ([]types.BlobInfo, error) { +func BlobInfoFromImage(ctx context.Context, sysCtx *types.SystemContext, img types.Image) ([]types.BlobInfo, error) { var layerInfos []types.BlobInfo // For containers-storage transport, use LayerInfosForCopy to get storage-accessible digests // For other transports (like docker), use LayerInfos which works fine - ref := img.Reference() - if ref.Transport().Name() == "containers-storage" { + if img.Reference().Transport().Name() == storageTransport.Transport.Name() { imgSrc, err := img.Reference().NewImageSource(ctx, sysCtx) if err != nil { return nil, fmt.Errorf("failed to create image source for containers-storage transport: %w", err) @@ -144,9 +141,7 @@ func BlobInfoFromImage(img types.Image, ctx context.Context, sysCtx *types.Syste // Convert LayerInfo to BlobInfo for consistency imgLayerInfos := img.LayerInfos() layerInfos = make([]types.BlobInfo, len(imgLayerInfos)) - for i, layer := range imgLayerInfos { - layerInfos[i] = layer - } + copy(layerInfos, imgLayerInfos) } return layerInfos, nil } @@ -164,3 +159,19 @@ func FormatDigest(digest digest.Digest, fullDigest bool) string { } return encoded } + +// HumanReadableSize converts a byte count to a human readable string +// From https://yourbasic.org/golang/formatting-byte-size-to-human-readable-format/ +func HumanReadableSize(b int64) string { + const unit = 1000 + if b < unit { + return fmt.Sprintf("%d B", b) + } + div, exp := int64(unit), 0 + for n := b / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.1f %cB", + float64(b)/float64(div), "kMGTPE"[exp]) +}