diff --git a/cmd/create.go b/cmd/create.go index 8e39544..2723ee1 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -3,6 +3,8 @@ package cmd import ( "fmt" "os" + "path/filepath" + "strings" ocidelta "github.com/containers/oci-delta/pkg/oci-delta" "github.com/spf13/cobra" @@ -11,6 +13,7 @@ import ( var ( createVerbose bool createDebug bool + createQuiet bool createParallelism int createSignatures []string ) @@ -37,6 +40,12 @@ func init() { createCmd.Flags().BoolVar(&createDebug, "debug", false, "show detailed progress information") createCmd.Flags().IntVarP(&createParallelism, "jobs", "j", 0, "max parallel tar-diff workers (default: number of CPUs)") createCmd.Flags().StringArrayVar(&createSignatures, "signature", nil, "signature OCI artifact to embed (can be specified multiple times)") + createCmd.Flags().BoolVarP(&createQuiet, "quiet", "q", false, "suppress all default output") +} + +// Used in info output for simpler feedback. +func bytesToMB(b int64) string { + return fmt.Sprintf("%.2f MB", float64(b)/(1000*1000)) } func runCreate(cmd *cobra.Command, args []string) error { @@ -46,7 +55,7 @@ func runCreate(cmd *cobra.Command, args []string) error { } defer os.RemoveAll(tmpDir) - log := &cmdLogger{debug: createDebug} + log := &cmdLogger{debug: createDebug, quiet: createQuiet} log.Debug("Opening old image: %s", args[0]) @@ -66,6 +75,10 @@ func runCreate(cmd *cobra.Command, args []string) error { sigReaders := ocidelta.ExtractedSignatures(newReader) + if len(createSignatures) > 0 { + log.Info("Embedding %d signature(s)", len(createSignatures)) + } + for _, sigPath := range createSignatures { log.Debug("Opening signature: %s", sigPath) @@ -96,6 +109,16 @@ func runCreate(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to write output: %w", err) } + if !createVerbose && stats != nil { + if stats.ProcessedLayerBytes > 0 { + saved := stats.ProcessedLayerBytes - stats.TarDiffLayerBytes - stats.OriginalLayerBytes + log.Info("\nDelta complete: %d layer(s) diffed, %d reused, bytes saved %.2f%% (%s → %s)", stats.ProcessedLayers, stats.SkippedLayers, + float64(saved)/float64(stats.ProcessedLayerBytes)*100, bytesToMB(stats.ProcessedLayerBytes), bytesToMB(stats.ProcessedLayerBytes-saved)) + } else { + log.Info("\nDelta complete: %d layer(s) diffed, %d reused", stats.ProcessedLayers, stats.SkippedLayers) + } + } + if createVerbose && stats != nil { fmt.Printf("\nDelta creation statistics:\n") fmt.Printf(" Old image layers: %d\n", stats.OldLayers) @@ -109,9 +132,21 @@ func runCreate(cmd *cobra.Command, args []string) error { if stats.ProcessedLayerBytes > 0 { saved := stats.ProcessedLayerBytes - stats.TarDiffLayerBytes - stats.OriginalLayerBytes pct := float64(saved) / float64(stats.ProcessedLayerBytes) * 100 - fmt.Printf(" Bytes saved: %d (%.1f%%)\n", saved, pct) + fmt.Printf(" Bytes saved: %d (%.1f%%)\n\n", saved, pct) } } + log.Info("Delta written to %s", args[2]) + + outputPath := strings.TrimPrefix(strings.TrimPrefix(args[2], "oci-archive:"), "oci:") + if p, _, ok := strings.Cut(outputPath, ":"); ok { + outputPath = p + } + + if abs, err := filepath.Abs(outputPath); err == nil { + outputPath = abs + } + log.Debug("Delta filepath output: %s", outputPath) + return nil } diff --git a/cmd/create_test.go b/cmd/create_test.go new file mode 100644 index 0000000..d218411 --- /dev/null +++ b/cmd/create_test.go @@ -0,0 +1,23 @@ +package cmd + +import ( + "testing" +) + +func TestBytesToMB(t *testing.T) { + tests := []struct { + bytes int64 + want string + }{ + {0, "0.00 MB"}, + {1_000_000, "1.00 MB"}, + {1_500_000, "1.50 MB"}, + {1_234_000, "1.23 MB"}, + {1_239_000, "1.24 MB"}, + } + for _, tt := range tests { + if got := bytesToMB(tt.bytes); got != tt.want { + t.Errorf("bytesToMB(%d) = %q, want %q", tt.bytes, got, tt.want) + } + } +} diff --git a/cmd/root.go b/cmd/root.go index 0504e94..7d6934a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -40,11 +40,20 @@ func Root() *cobra.Command { type Logger interface { Debug(format string, args ...interface{}) Warning(format string, args ...interface{}) + Info(format string, args ...interface{}) } // cmdLogger implements the Logger interface type cmdLogger struct { debug bool + quiet bool +} + +func (l *cmdLogger) Info(format string, args ...interface{}) { + // Debug is a much more detailed version of the info logs, no need for both to exist at the same time. + if !l.quiet && !l.debug { + fmt.Printf(format+"\n", args...) + } } func (l *cmdLogger) Debug(format string, args ...interface{}) { diff --git a/pkg/oci-delta/common.go b/pkg/oci-delta/common.go index 0654426..4cb44d6 100644 --- a/pkg/oci-delta/common.go +++ b/pkg/oci-delta/common.go @@ -14,12 +14,14 @@ import ( type Logger interface { Debug(format string, args ...interface{}) + Info(format string, args ...interface{}) Warning(format string, args ...interface{}) } type SilentLogger struct{} func (SilentLogger) Debug(string, ...interface{}) {} +func (SilentLogger) Info(string, ...interface{}) {} func (SilentLogger) Warning(string, ...interface{}) {} var ociLayoutFileData = []byte(`{"imageLayoutVersion":"1.0.0"}`) diff --git a/pkg/oci-delta/create.go b/pkg/oci-delta/create.go index 51524be..c397131 100644 --- a/pkg/oci-delta/create.go +++ b/pkg/oci-delta/create.go @@ -68,6 +68,9 @@ func CreateDelta(oldReader OCIReader, newReader OCIReader, writer OCIWriter, opt log.Debug("Layers with new content (will process): %d", stats.ProcessedLayers) log.Debug("Layers with existing content (will skip): %d", stats.SkippedLayers) + log.Info("Found %d layer(s) to diff, %d layer(s) unchanged", stats.ProcessedLayers, stats.SkippedLayers) + + log.Info("\nProcessing layers...") log.Debug("\nProcessing layers...") for _, l := range new.layers { @@ -76,6 +79,8 @@ func CreateDelta(oldReader OCIReader, newReader OCIReader, writer OCIWriter, opt } } + log.Info(" Skipped %d layer(s) with existing content", stats.SkippedLayers) + layerResults, err := computeLayerDiffsParallel(log, old, new, newOnlyLayers, opts.TmpDir, opts.Parallelism) if err != nil { return nil, err @@ -407,6 +412,7 @@ func computeLayerDiff(log Logger, old *OCIImage, new *OCIImage, blobDigest diges } sizeReader.Close() + log.Info(" Computing diff for layer %d/%d", layerNum, total) log.Debug(" Computing diff for layer %d/%d %s (%d bytes)", layerNum, total, blobDigest.Encoded()[:16], originalSize) tmpFile, err := os.CreateTemp(tmpDir, "oci-delta-*.tar-diff") diff --git a/pkg/oci-delta/verify_test.go b/pkg/oci-delta/verify_test.go index a72d639..3b74938 100644 --- a/pkg/oci-delta/verify_test.go +++ b/pkg/oci-delta/verify_test.go @@ -22,6 +22,7 @@ import ( type testLogger struct{} func (l *testLogger) Debug(format string, args ...interface{}) {} +func (l *testLogger) Info(format string, args ...interface{}) {} func (l *testLogger) Warning(format string, args ...interface{}) {} func createTestKey(t *testing.T) (sigstoreSignature.Verifier, *ecdsa.PrivateKey) {