From fa8c0beddf83bc676f78195d5b539c030cd38b94 Mon Sep 17 00:00:00 2001 From: Jens Arnfast Date: Tue, 7 May 2024 20:59:53 +0200 Subject: [PATCH] Support configuring the compression level when archiving bundles Signed-off-by: Jens Arnfast --- cmd/porter/bundle.go | 7 +++-- pkg/porter/archive.go | 46 ++++++++++++++++++++++++++++--- pkg/porter/archive_test.go | 20 ++++++++------ tests/integration/archive_test.go | 42 ++++++++++++++++++++++++++++ 4 files changed, 101 insertions(+), 14 deletions(-) diff --git a/cmd/porter/bundle.go b/cmd/porter/bundle.go index 237e81b78..eabfdec26 100644 --- a/cmd/porter/bundle.go +++ b/cmd/porter/bundle.go @@ -190,6 +190,7 @@ func buildBundleArchiveCommand(p *porter.Porter) *cobra.Command { Long: "Archives a bundle by generating a gzipped tar archive containing the bundle, invocation image and any referenced images.", Example: ` porter bundle archive mybun.tgz --reference ghcr.io/getporter/examples/porter-hello:v0.2.0 porter bundle archive mybun.tgz --reference localhost:5000/ghcr.io/getporter/examples/porter-hello:v0.2.0 --force + porter bundle archive mybun.tgz --compression NoCompression --reference ghcr.io/getporter/examples/porter-hello:v0.2.0 `, PreRunE: func(cmd *cobra.Command, args []string) error { return opts.Validate(cmd.Context(), args, p) @@ -199,7 +200,9 @@ func buildBundleArchiveCommand(p *porter.Porter) *cobra.Command { }, } - addBundlePullFlags(cmd.Flags(), &opts.BundlePullOptions) - + f := cmd.Flags() + addBundlePullFlags(f, &opts.BundlePullOptions) + f.StringVarP(&opts.CompressionLevel, "compression", "c", opts.GetCompressionLevelDefault(), + fmt.Sprintf("Compression level to use when creating the gzipped tar archive. Allowed values are: %s", strings.Join(opts.GetCompressionLevelAllowedValues(), ", "))) return &cmd } diff --git a/pkg/porter/archive.go b/pkg/porter/archive.go index 454923d85..799c17f59 100644 --- a/pkg/porter/archive.go +++ b/pkg/porter/archive.go @@ -31,7 +31,30 @@ import ( // ArchiveOptions defines the valid options for performing an archive operation type ArchiveOptions struct { BundleReferenceOptions - ArchiveFile string + ArchiveFile string + CompressionLevel string + compressionLevelInt int +} + +var compressionLevelValues = map[string]int{ + "NoCompression": gzip.NoCompression, + "BestSpeed": gzip.BestSpeed, + "BestCompression": gzip.BestCompression, + "DefaultCompression": gzip.DefaultCompression, + "HuffmanOnly": gzip.HuffmanOnly, +} + +func (o *ArchiveOptions) GetCompressionLevelDefault() string { + return "DefaultCompression" +} + +func (p *ArchiveOptions) GetCompressionLevelAllowedValues() []string { + levels := make([]string, 0, len(compressionLevelValues)) + for level := range compressionLevelValues { + levels = append(levels, level) + } + sort.Strings(levels) + return levels } // Validate performs validation on the publish options @@ -47,6 +70,16 @@ func (o *ArchiveOptions) Validate(ctx context.Context, args []string, p *Porter) if o.Reference == "" { return errors.New("must provide a value for --reference of the form REGISTRY/bundle:tag") } + + if o.CompressionLevel == "" { + o.CompressionLevel = o.GetCompressionLevelDefault() + } + level, ok := compressionLevelValues[o.CompressionLevel] + if !ok { + return fmt.Errorf("invalid compression level: %s", o.CompressionLevel) + } + o.compressionLevelInt = level + return o.BundleReferenceOptions.Validate(ctx, args, p) } @@ -87,6 +120,7 @@ func (p *Porter) Archive(ctx context.Context, opts ArchiveOptions) error { destination: dest, imageStoreConstructor: ctor, insecureRegistry: opts.InsecureRegistry, + compressionLevel: opts.compressionLevelInt, } if err := exp.export(ctx); err != nil { return log.Error(err) @@ -105,6 +139,7 @@ type exporter struct { imageStoreConstructor imagestore.Constructor imageStore imagestore.Store insecureRegistry bool + compressionLevel int } func (ex *exporter) export(ctx context.Context) error { @@ -156,7 +191,7 @@ func (ex *exporter) export(ctx context.Context) error { return fmt.Errorf("error preparing bundle artifact: %s", err) } - rc, err := ex.CustomTar(ctx, archiveDir) + rc, err := ex.CustomTar(ctx, archiveDir, ex.compressionLevel) if err != nil { return fmt.Errorf("error creating archive: %w", err) } @@ -219,10 +254,13 @@ func (ex *exporter) createTarHeader(ctx context.Context, path string, file strin return header, nil } -func (ex *exporter) CustomTar(ctx context.Context, srcPath string) (io.ReadCloser, error) { +func (ex *exporter) CustomTar(ctx context.Context, srcPath string, compressionLevel int) (io.ReadCloser, error) { pipeReader, pipeWriter := io.Pipe() - gzipWriter := gzip.NewWriter(pipeWriter) + gzipWriter, err := gzip.NewWriterLevel(pipeWriter, compressionLevel) + if err != nil { + return nil, err + } tarWriter := tar.NewWriter(gzipWriter) cleanSrcPath := filepath.Clean(srcPath) diff --git a/pkg/porter/archive_test.go b/pkg/porter/archive_test.go index 0ca8b998d..60fecd6ff 100644 --- a/pkg/porter/archive_test.go +++ b/pkg/porter/archive_test.go @@ -32,21 +32,25 @@ func TestArchive_Validate(t *testing.T) { defer p.Close() testcases := []struct { - name string - args []string - reference string - wantError string + name string + args []string + reference string + compressionLevel string + wantError string }{ - {"no arg", nil, "", "destination file is required"}, - {"no tag", []string{"/path/to/file"}, "", "must provide a value for --reference of the form REGISTRY/bundle:tag"}, - {"too many args", []string{"/path/to/file", "moar args!"}, "myreg/mybuns:v0.1.0", "only one positional argument may be specified, the archive file name, but multiple were received: [/path/to/file moar args!]"}, - {"just right", []string{"/path/to/file"}, "myreg/mybuns:v0.1.0", ""}, + {"no arg", nil, "", "", "destination file is required"}, + {"no tag", []string{"/path/to/file"}, "", "", "must provide a value for --reference of the form REGISTRY/bundle:tag"}, + {"too many args", []string{"/path/to/file", "moar args!"}, "myreg/mybuns:v0.1.0", "", "only one positional argument may be specified, the archive file name, but multiple were received: [/path/to/file moar args!]"}, + {"invalid compression level", []string{"/path/to/file"}, "myreg/mybuns:v0.1.0", "NotValidCompression", "invalid compression level: NotValidCompression"}, + {"no compression level", []string{"/path/to/file"}, "myreg/mybuns:v0.1.0", "NoCompression", ""}, + {"just right", []string{"/path/to/file"}, "myreg/mybuns:v0.1.0", "", ""}, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { opts := ArchiveOptions{} opts.Reference = tc.reference + opts.CompressionLevel = tc.compressionLevel err := opts.Validate(context.Background(), tc.args, p.Porter) if tc.wantError != "" { diff --git a/tests/integration/archive_test.go b/tests/integration/archive_test.go index f3b03f5c9..ef9273e83 100644 --- a/tests/integration/archive_test.go +++ b/tests/integration/archive_test.go @@ -98,3 +98,45 @@ func getHash(p *porter.TestPorter, path string) string { return fmt.Sprintf("%x", h.Sum(nil)) } + +// Validate that a bundle archived with NoCompression can be published +func TestArchive_WithNoCompression(t *testing.T) { + t.Parallel() + p := porter.NewTestPorter(t) + defer p.Close() + ctx := p.SetupIntegrationTest() + + // Use a fixed bundle to work with so that we can rely on the registry and layer digests + const reference = "ghcr.io/getporter/examples/whalegap:v0.2.0" + + // Archive bundle + archiveOpts := porter.ArchiveOptions{} + archiveOpts.Reference = reference + archiveOpts.CompressionLevel = "NoCompression" + archiveFile := "mybuns1nocomp.tgz" + err := archiveOpts.Validate(ctx, []string{archiveFile}, p.Porter) + require.NoError(p.T(), err, "validation of archive opts for bundle failed") + + err = p.Archive(ctx, archiveOpts) + require.NoError(p.T(), err, "archival of bundle failed") + + hash := getHash(p, archiveFile) + + // different compressions yields different (but consistent) hashes + consistentHash := "191a249d861f41492ee568080a063718ad77e9b18ad0672cbf4fc2f0e4d1c07c" + assert.Equal(p.T(), consistentHash, hash, "shasum of archive did not match expected hash") + + // Publish bundle from archive, with new reference + localReference := "localhost:5000/archived-nocompression-whalegap:v0.2.0" + publishFromArchiveOpts := porter.PublishOptions{ + ArchiveFile: archiveFile, + BundlePullOptions: porter.BundlePullOptions{ + Reference: localReference, + }, + } + err = publishFromArchiveOpts.Validate(p.Config) + require.NoError(p.T(), err, "validation of publish opts for bundle failed") + + err = p.Publish(ctx, publishFromArchiveOpts) + require.NoError(p.T(), err, "publish of bundle from archive failed") +}