Skip to content

Commit

Permalink
Support configuring the compression level when archiving bundles
Browse files Browse the repository at this point in the history
Signed-off-by: Jens Arnfast <[email protected]>
  • Loading branch information
jarnfast committed May 7, 2024
1 parent 47919a1 commit fa8c0be
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 14 deletions.
7 changes: 5 additions & 2 deletions cmd/porter/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
}
46 changes: 42 additions & 4 deletions pkg/porter/archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}

Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
Expand Down
20 changes: 12 additions & 8 deletions pkg/porter/archive_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "" {
Expand Down
42 changes: 42 additions & 0 deletions tests/integration/archive_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}

0 comments on commit fa8c0be

Please sign in to comment.