Skip to content

Commit 60f4022

Browse files
Add zstd compression to rules_oci
1 parent 0c7dd51 commit 60f4022

11 files changed

Lines changed: 132 additions & 43 deletions

File tree

MODULE.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ use_repo(
2525
"com_github_containerd_containerd",
2626
"com_github_containerd_log",
2727
"com_github_docker_docker_credential_helpers",
28+
"com_github_klauspost_compress",
2829
"com_github_mitchellh_go_homedir",
2930
"com_github_opencontainers_go_digest",
3031
"com_github_opencontainers_image_spec",

docs/docs.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,8 @@ The config file named after the rule, os, and arch
173173
<pre>
174174
load("@rules_oci//oci:defs.bzl", "oci_image_layer")
175175

176-
oci_image_layer(<a href="#oci_image_layer-name">name</a>, <a href="#oci_image_layer-directory">directory</a>, <a href="#oci_image_layer-files">files</a>, <a href="#oci_image_layer-file_map">file_map</a>, <a href="#oci_image_layer-mode_map">mode_map</a>, <a href="#oci_image_layer-owner_map">owner_map</a>, <a href="#oci_image_layer-symlinks">symlinks</a>, <a href="#oci_image_layer-kwargs">kwargs</a>)
176+
oci_image_layer(<a href="#oci_image_layer-name">name</a>, <a href="#oci_image_layer-directory">directory</a>, <a href="#oci_image_layer-files">files</a>, <a href="#oci_image_layer-file_map">file_map</a>, <a href="#oci_image_layer-mode_map">mode_map</a>, <a href="#oci_image_layer-owner_map">owner_map</a>, <a href="#oci_image_layer-symlinks">symlinks</a>, <a href="#oci_image_layer-compression_method">compression_method</a>,
177+
<a href="#oci_image_layer-kwargs">kwargs</a>)
177178
</pre>
178179

179180
Creates a tarball and an OCI descriptor for it
@@ -190,6 +191,7 @@ Creates a tarball and an OCI descriptor for it
190191
| <a id="oci_image_layer-mode_map"></a>mode_map | Dictionary of file location in tarball -> mode int (e.g. 0o755) | `None` |
191192
| <a id="oci_image_layer-owner_map"></a>owner_map | Dictionary of file location in tarball -> owner:group string (e.g. '501:501') | `None` |
192193
| <a id="oci_image_layer-symlinks"></a>symlinks | Dictionary of symlink -> target entries to place in the tarball | `None` |
194+
| <a id="oci_image_layer-compression_method"></a>compression_method | A string, currently supports "gzip" and "zstd", defaults to "gzip" | `None` |
193195
| <a id="oci_image_layer-kwargs"></a>kwargs | Additional arguments to pass to the rule, e.g. tags or visibility | none |
194196

195197

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require (
1010
github.com/containerd/containerd v1.7.20
1111
github.com/containerd/log v0.1.0
1212
github.com/docker/docker-credential-helpers v0.8.1
13+
github.com/klauspost/compress v1.17.8
1314
github.com/mitchellh/go-homedir v1.1.0
1415
github.com/opencontainers/go-digest v1.0.0
1516
github.com/opencontainers/image-spec v1.1.0
@@ -41,7 +42,6 @@ require (
4142
github.com/go-logr/logr v1.4.2 // indirect
4243
github.com/go-logr/stdr v1.2.2 // indirect
4344
github.com/gorilla/mux v1.8.1 // indirect
44-
github.com/klauspost/compress v1.17.8 // indirect
4545
github.com/kr/text v0.2.0 // indirect
4646
github.com/moby/locker v1.0.1 // indirect
4747
github.com/pkg/errors v0.9.1 // indirect

go/cmd/ocitool/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ go_library(
3131
"@com_github_containerd_containerd//images:go_default_library",
3232
"@com_github_containerd_containerd//platforms:go_default_library",
3333
"@com_github_containerd_log//:go_default_library",
34+
"@com_github_klauspost_compress//zstd:go_default_library",
3435
"@com_github_opencontainers_go_digest//:go_default_library",
3536
"@com_github_opencontainers_image_spec//specs-go:go_default_library",
3637
"@com_github_opencontainers_image_spec//specs-go/v1:go_default_library",

go/cmd/ocitool/createlayer_cmd.go

Lines changed: 55 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/DataDog/rules_oci/go/internal/tarutil"
1818
"github.com/DataDog/rules_oci/go/pkg/layer"
1919
"github.com/DataDog/rules_oci/go/pkg/ociutil"
20+
"github.com/klauspost/compress/zstd"
2021
"github.com/opencontainers/go-digest"
2122
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
2223
"github.com/urfave/cli/v2"
@@ -35,11 +36,32 @@ func CreateLayerCmd(c *cli.Context) error {
3536

3637
digester := digest.SHA256.Digester()
3738
wc := ociutil.NewWriterCounter(io.MultiWriter(out, digester.Hash()))
38-
gw := gzip.NewWriter(wc)
39-
gw.Name = path.Base(out.Name())
40-
defer gw.Close()
4139

42-
tw := tar.NewWriter(gw)
40+
var compressWriter io.Writer
41+
var compressCloser io.Closer
42+
var mediaType string
43+
switch config.CompressionMethod {
44+
case "gzip":
45+
gzipWriter := gzip.NewWriter(wc)
46+
gzipWriter.Name = path.Base(out.Name())
47+
compressWriter = gzipWriter
48+
compressCloser = gzipWriter
49+
mediaType = ocispec.MediaTypeImageLayerGzip
50+
case "zstd":
51+
zstdWriter, err := zstd.NewWriter(wc,
52+
zstd.WithEncoderLevel(zstd.SpeedBestCompression))
53+
if err != nil {
54+
return err
55+
}
56+
compressWriter = zstdWriter
57+
compressCloser = zstdWriter
58+
mediaType = ocispec.MediaTypeImageLayerZstd
59+
default:
60+
return fmt.Errorf("uknown compress method %s", config.CompressionMethod)
61+
}
62+
defer compressCloser.Close()
63+
64+
tw := tar.NewWriter(compressWriter)
4365
defer tw.Close()
4466

4567
slices.Sort(config.Files)
@@ -139,11 +161,12 @@ func CreateLayerCmd(c *cli.Context) error {
139161
// Need to flush before we count bytes and digest, might as well close since
140162
// it's not needed anymore.
141163
tw.Close()
142-
gw.Close()
164+
compressCloser.Close()
165+
out.Close()
143166

144167
desc := ocispec.Descriptor{
145168
Digest: digester.Digest(),
146-
MediaType: ocispec.MediaTypeImageLayerGzip,
169+
MediaType: mediaType,
147170
Size: int64(wc.Count()),
148171
}
149172

@@ -164,15 +187,16 @@ func CreateLayerCmd(c *cli.Context) error {
164187
}
165188

166189
type createLayerConfig struct {
167-
BazelLabel string `json:"bazel-label" toml:"bazel-label" yaml:"bazel-label"`
168-
Descriptor string `json:"outd" toml:"outd" yaml:"outd"`
169-
Directory string `json:"dir" toml:"dir" yaml:"dir"`
170-
FileMapping map[string]string `json:"file-map" toml:"file-map" yaml:"file-map"`
171-
Files []string `json:"file" toml:"file" yaml:"file"`
172-
ModeMapping map[string]int64 `json:"mode-map" toml:"mode-map" yaml:"mode-map"`
173-
OutputLayer string `json:"out" toml:"out" yaml:"out"`
174-
OwnerMapping map[string]string `json:"owner-map" toml:"owner-map" yaml:"owner-map"`
175-
SymlinkMapping map[string]string `json:"symlink" toml:"symlink" yaml:"symlink"`
190+
BazelLabel string `json:"bazel-label" toml:"bazel-label" yaml:"bazel-label"`
191+
Descriptor string `json:"outd" toml:"outd" yaml:"outd"`
192+
Directory string `json:"dir" toml:"dir" yaml:"dir"`
193+
FileMapping map[string]string `json:"file-map" toml:"file-map" yaml:"file-map"`
194+
Files []string `json:"file" toml:"file" yaml:"file"`
195+
ModeMapping map[string]int64 `json:"mode-map" toml:"mode-map" yaml:"mode-map"`
196+
OutputLayer string `json:"out" toml:"out" yaml:"out"`
197+
OwnerMapping map[string]string `json:"owner-map" toml:"owner-map" yaml:"owner-map"`
198+
SymlinkMapping map[string]string `json:"symlink" toml:"symlink" yaml:"symlink"`
199+
CompressionMethod string `json:"compression-method" toml:"compression-method" yaml:"compression-method"`
176200
}
177201

178202
func newCreateLayerConfig(c *cli.Context) (*createLayerConfig, error) {
@@ -184,16 +208,23 @@ func newCreateLayerConfig(c *cli.Context) (*createLayerConfig, error) {
184208
}
185209
modeMapping[path] = mode
186210
}
211+
212+
compressionMethod := c.String("compression-method")
213+
if compressionMethod == "" {
214+
compressionMethod = "gzip"
215+
}
216+
187217
return &createLayerConfig{
188-
BazelLabel: c.String("bazel-label"),
189-
Descriptor: c.String("outd"),
190-
Directory: c.String("dir"),
191-
FileMapping: c.Generic("file-map").(*flagutil.KeyValueFlag).Map,
192-
Files: c.StringSlice("file"),
193-
ModeMapping: modeMapping,
194-
OutputLayer: c.String("out"),
195-
OwnerMapping: c.Generic("owner-map").(*flagutil.KeyValueFlag).Map,
196-
SymlinkMapping: c.Generic("symlink").(*flagutil.KeyValueFlag).Map,
218+
BazelLabel: c.String("bazel-label"),
219+
Descriptor: c.String("outd"),
220+
Directory: c.String("dir"),
221+
FileMapping: c.Generic("file-map").(*flagutil.KeyValueFlag).Map,
222+
Files: c.StringSlice("file"),
223+
ModeMapping: modeMapping,
224+
OutputLayer: c.String("out"),
225+
OwnerMapping: c.Generic("owner-map").(*flagutil.KeyValueFlag).Map,
226+
SymlinkMapping: c.Generic("symlink").(*flagutil.KeyValueFlag).Map,
227+
CompressionMethod: compressionMethod,
197228
}, nil
198229
}
199230

go/cmd/ocitool/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ var app = &cli.App{
7878
Name: "mode-map",
7979
Value: &flagutil.KeyValueFlag{},
8080
},
81+
&cli.StringFlag{
82+
Name: "compression-method",
83+
},
8184
},
8285
},
8386
{

go/pkg/ociutil/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ go_library(
3535
"@com_github_containerd_containerd//reference/docker:go_default_library",
3636
"@com_github_containerd_containerd//remotes:go_default_library",
3737
"@com_github_containerd_containerd//remotes/docker:go_default_library",
38+
"@com_github_klauspost_compress//zstd:go_default_library",
3839
"@com_github_opencontainers_go_digest//:go_default_library",
3940
"@com_github_opencontainers_image_spec//specs-go/v1:go_default_library",
4041
"@com_github_sethvargo_go_retry//:go_default_library",

go/pkg/ociutil/diff.go

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import (
44
"compress/gzip"
55
"context"
66
"fmt"
7+
"io"
78

89
"github.com/containerd/containerd/content"
10+
"github.com/klauspost/compress/zstd"
911
"github.com/opencontainers/go-digest"
1012
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
1113
)
@@ -14,23 +16,31 @@ import (
1416
// image config. If a layer is uncompressed, the diff ID is simply the digest but if the
1517
// layer is compressed, we must uncompress the file and acquire the digest.
1618
func GetLayerDiffID(ctx context.Context, store content.Store, desc ocispec.Descriptor) (digest.Digest, error) {
19+
if desc.MediaType != ocispec.MediaTypeImageLayerGzip && desc.MediaType != ocispec.MediaTypeImageLayerZstd {
20+
return desc.Digest, nil
21+
}
22+
23+
r, err := store.ReaderAt(ctx, desc)
24+
if err != nil {
25+
return "", fmt.Errorf("failed to get reader for layer: %w", err)
26+
}
27+
defer r.Close()
28+
29+
var cr io.Reader
1730
switch desc.MediaType {
1831
case ocispec.MediaTypeImageLayerGzip:
19-
r, err := store.ReaderAt(ctx, desc)
32+
cr, err = gzip.NewReader(&readerAtReader{ReaderAt: r})
2033
if err != nil {
21-
return "", fmt.Errorf("failed to get reader for layer: %w", err)
34+
return "", fmt.Errorf("failed to get gzip reader for layer: %w", err)
2235
}
23-
defer r.Close()
24-
25-
gr, err := gzip.NewReader(&readerAtReader{ReaderAt: r})
36+
case ocispec.MediaTypeImageLayerZstd:
37+
cr, err = zstd.NewReader(&readerAtReader{ReaderAt: r})
2638
if err != nil {
27-
return "", fmt.Errorf("failed to get gzip reader for layer: %w", err)
39+
return "", fmt.Errorf("failed to get zstd reader for layer: %w", err)
2840
}
29-
30-
return digest.SHA256.FromReader(gr)
31-
default:
32-
return desc.Digest, nil
3341
}
42+
43+
return digest.SHA256.FromReader(cr)
3444
}
3545

3646
type readerAtReader struct {

oci/layer.bzl

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ def oci_image_layer(
1111
mode_map = None, # dict[str, int] | None,
1212
owner_map = None, # dict[str, str] | None,
1313
symlinks = None, # dict[str, str] | None,
14+
compression_method = None, # str | None
1415
**kwargs):
1516
""" Creates a tarball and an OCI descriptor for it
1617
@@ -22,6 +23,7 @@ def oci_image_layer(
2223
mode_map: Dictionary of file location in tarball -> mode int (e.g. 0o755)
2324
owner_map: Dictionary of file location in tarball -> owner:group string (e.g. '501:501')
2425
symlinks: Dictionary of symlink -> target entries to place in the tarball
26+
compression_method: A string, currently supports "gzip" and "zstd", defaults to "gzip"
2527
**kwargs: Additional arguments to pass to the rule, e.g. tags or visibility
2628
"""
2729
mode_map = {k: str(v) for k, v in mode_map.items()} if mode_map else {}
@@ -34,22 +36,41 @@ def oci_image_layer(
3436
mode_map = mode_map,
3537
owner_map = owner_map,
3638
symlinks = symlinks,
39+
compression_method = compression_method,
3740
**kwargs
3841
)
3942

43+
def _get_output_file_name(name, compression_method):
44+
file_extension = {
45+
"gzip": "gz",
46+
"zstd": "zst",
47+
}.get(compression_method)
48+
49+
if not file_extension:
50+
fail("Unknown compression_method: {}".format(compression_method))
51+
52+
return "{}-layer.tar.{}".format(name, file_extension)
53+
4054
def _impl(ctx):
4155
toolchain = ctx.toolchains["@com_github_datadog_rules_oci//oci:toolchain"]
4256

4357
descriptor_file = ctx.actions.declare_file("{}.descriptor.json".format(ctx.label.name))
4458

59+
compression_method = ctx.attr.compression_method
60+
if not compression_method:
61+
compression_method = "gzip"
62+
63+
output_file = ctx.actions.declare_file(_get_output_file_name(ctx.label.name, compression_method))
64+
4565
ctx.actions.run(
4666
executable = toolchain.sdk.ocitool,
4767
arguments = [
4868
"create-layer",
4969
"--bazel-label={}".format(ctx.label),
5070
"--dir={}".format(ctx.attr.directory),
51-
"--out={}".format(ctx.outputs.layer.path),
71+
"--out={}".format(output_file.path),
5272
"--outd={}".format(descriptor_file.path),
73+
"--compression-method={}".format(compression_method),
5374
] +
5475
["--file-map={}={}".format(k.files.to_list()[0].path, v) for k, v in ctx.attr.file_map.items()] +
5576
["--file={}".format(f.path) for f in ctx.files.files] +
@@ -60,17 +81,24 @@ def _impl(ctx):
6081
mnemonic = "OCIImageCreateLayer",
6182
outputs = [
6283
descriptor_file,
63-
ctx.outputs.layer,
84+
output_file,
6485
],
6586
)
6687

6788
return [
6889
OCIDescriptor(
6990
descriptor_file = descriptor_file,
70-
file = ctx.outputs.layer,
91+
file = output_file,
7192
),
7293
]
7394

95+
def _get_outputs(name, compression_method):
96+
if not compression_method:
97+
compression_method = "gzip"
98+
return {
99+
"layer": _get_output_file_name(name, compression_method),
100+
}
101+
74102
_oci_image_layer = rule(
75103
implementation = _impl,
76104
doc = "Create a tarball and an OCI descriptor for it",
@@ -81,9 +109,8 @@ _oci_image_layer = rule(
81109
"mode_map": attr.string_dict(),
82110
"owner_map": attr.string_dict(),
83111
"symlinks": attr.string_dict(),
112+
"compression_method": attr.string(),
84113
},
114+
outputs = _get_outputs,
85115
toolchains = ["@com_github_datadog_rules_oci//oci:toolchain"],
86-
outputs = {
87-
"layer": "%{name}-layer.tar.gz",
88-
},
89116
)

tests/go-multiarch-image/BUILD.bazel

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,18 @@ go_multiarch_image(
2727
visibility = ["//visibility:public"],
2828
)
2929

30+
go_multiarch_image(
31+
name = "zstd-image",
32+
archs = [
33+
"amd64",
34+
"arm64",
35+
],
36+
base = "@ubuntu_focal//image",
37+
compression_method = "zstd",
38+
embed = [":go_default_library"],
39+
visibility = ["//visibility:public"],
40+
)
41+
3042
oci_push(
3143
name = "push",
3244
manifest = ":image",

0 commit comments

Comments
 (0)