Skip to content

Commit 2ad7f24

Browse files
committed
exporter: add Docker compatible exporter
Signed-off-by: Tonis Tiigi <[email protected]>
1 parent cbe4cf0 commit 2ad7f24

File tree

7 files changed

+354
-56
lines changed

7 files changed

+354
-56
lines changed

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,18 @@ If credentials are required, `buildctl` will attempt to read Docker configuratio
123123
buildctl build ... --exporter=local --exporter-opt output=path/to/output-dir
124124
```
125125
126-
#### Exporting OCI Image Format tarball to client
126+
##### Exporting build result to Docker
127+
128+
```
129+
# exported tarball is also compatible with OCI spec
130+
buildctl build ... --exporter=docker --exporter-opt name=myimage | docker load
131+
```
132+
133+
##### Exporting OCI Image Format tarball to client
127134
128135
```
129136
buildctl build ... --exporter=oci --exporter-opt output=path/to/output.tar
130-
buildctl build ... --exporter=oci > output.tar
137+
buildctl build ... --exporter=oci --exporter-opt name=docker.io/user/repo:tag > output.tar
131138
```
132139
133140
#### View build cache

client/client_test.go

Lines changed: 73 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -275,53 +275,81 @@ func testOCIExporter(t *testing.T, sb integration.Sandbox) {
275275
def, err := st.Marshal()
276276
require.NoError(t, err)
277277

278-
destDir, err := ioutil.TempDir("", "buildkit")
279-
require.NoError(t, err)
280-
defer os.RemoveAll(destDir)
281-
282-
out := filepath.Join(destDir, "out.tar")
283-
target := "docker.io/buildkit/testoci:latest"
284-
285-
err = c.Solve(context.TODO(), def, SolveOpt{
286-
Exporter: ExporterOCI,
287-
ExporterAttrs: map[string]string{
288-
"output": out,
289-
"name": target,
290-
},
291-
}, nil)
292-
require.NoError(t, err)
293-
294-
dt, err := ioutil.ReadFile(out)
295-
require.NoError(t, err)
296-
297-
m, err := readTarToMap(dt, false)
298-
require.NoError(t, err)
299-
300-
_, ok := m["oci-layout"]
301-
require.True(t, ok)
302-
303-
var index ocispec.Index
304-
err = json.Unmarshal(m["index.json"].data, &index)
305-
require.NoError(t, err)
306-
require.Equal(t, 2, index.SchemaVersion)
307-
require.Equal(t, 1, len(index.Manifests))
308-
309-
var mfst ocispec.Manifest
310-
err = json.Unmarshal(m["blobs/sha256/"+index.Manifests[0].Digest.Hex()].data, &mfst)
311-
require.NoError(t, err)
312-
require.Equal(t, 2, len(mfst.Layers))
313-
require.Equal(t, target, index.Manifests[0].Annotations["org.opencontainers.image.ref.name"])
278+
for _, exp := range []string{ExporterOCI, ExporterDocker} {
279+
280+
destDir, err := ioutil.TempDir("", "buildkit")
281+
require.NoError(t, err)
282+
defer os.RemoveAll(destDir)
283+
284+
out := filepath.Join(destDir, "out.tar")
285+
target := "docker.io/buildkit/testoci:latest"
286+
287+
err = c.Solve(context.TODO(), def, SolveOpt{
288+
Exporter: exp,
289+
ExporterAttrs: map[string]string{
290+
"output": out,
291+
"name": target,
292+
},
293+
}, nil)
294+
require.NoError(t, err)
295+
296+
dt, err := ioutil.ReadFile(out)
297+
require.NoError(t, err)
298+
299+
m, err := readTarToMap(dt, false)
300+
require.NoError(t, err)
301+
302+
_, ok := m["oci-layout"]
303+
require.True(t, ok)
304+
305+
var index ocispec.Index
306+
err = json.Unmarshal(m["index.json"].data, &index)
307+
require.NoError(t, err)
308+
require.Equal(t, 2, index.SchemaVersion)
309+
require.Equal(t, 1, len(index.Manifests))
310+
311+
var mfst ocispec.Manifest
312+
err = json.Unmarshal(m["blobs/sha256/"+index.Manifests[0].Digest.Hex()].data, &mfst)
313+
require.NoError(t, err)
314+
require.Equal(t, 2, len(mfst.Layers))
315+
require.Equal(t, target, index.Manifests[0].Annotations["org.opencontainers.image.ref.name"])
316+
317+
var ociimg ocispec.Image
318+
err = json.Unmarshal(m["blobs/sha256/"+mfst.Config.Digest.Hex()].data, &ociimg)
319+
require.NoError(t, err)
320+
require.Equal(t, "layers", ociimg.RootFS.Type)
321+
require.Equal(t, 2, len(ociimg.RootFS.DiffIDs))
322+
323+
_, ok = m["blobs/sha256/"+mfst.Layers[0].Digest.Hex()]
324+
require.True(t, ok)
325+
_, ok = m["blobs/sha256/"+mfst.Layers[1].Digest.Hex()]
326+
require.True(t, ok)
327+
328+
if exp != ExporterDocker {
329+
continue
330+
}
314331

315-
var ociimg ocispec.Image
316-
err = json.Unmarshal(m["blobs/sha256/"+mfst.Config.Digest.Hex()].data, &ociimg)
317-
require.NoError(t, err)
318-
require.Equal(t, "layers", ociimg.RootFS.Type)
319-
require.Equal(t, 2, len(ociimg.RootFS.DiffIDs))
332+
var dockerMfst []struct {
333+
Config string
334+
RepoTags []string
335+
Layers []string
336+
}
337+
err = json.Unmarshal(m["manifest.json"].data, &dockerMfst)
338+
require.NoError(t, err)
339+
require.Equal(t, 1, len(dockerMfst))
340+
341+
_, ok = m[dockerMfst[0].Config]
342+
require.True(t, ok)
343+
require.Equal(t, 2, len(dockerMfst[0].Layers))
344+
require.Equal(t, 1, len(dockerMfst[0].RepoTags))
345+
require.Equal(t, target, dockerMfst[0].RepoTags[0])
346+
347+
for _, l := range dockerMfst[0].Layers {
348+
_, ok := m[l]
349+
require.True(t, ok)
350+
}
320351

321-
_, ok = m["blobs/sha256/"+mfst.Layers[0].Digest.Hex()]
322-
require.True(t, ok)
323-
_, ok = m["blobs/sha256/"+mfst.Layers[1].Digest.Hex()]
324-
require.True(t, ok)
352+
}
325353
}
326354

327355
func testBuildPushAndValidate(t *testing.T, sb integration.Sandbox) {

client/exporters.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package client
22

33
const (
4-
ExporterImage = "image"
5-
ExporterLocal = "local"
6-
ExporterOCI = "oci"
4+
ExporterImage = "image"
5+
ExporterLocal = "local"
6+
ExporterOCI = "oci"
7+
ExporterDocker = "docker"
78

89
exporterLocalOutputDir = "output"
910
exporterOCIDestination = "output"

client/solve.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ func (c *Client) Solve(ctx context.Context, def *llb.Definition, opt SolveOpt, s
7979
return errors.Errorf("output directory is required for local exporter")
8080
}
8181
s.Allow(filesync.NewFSSyncTarget(outputDir))
82-
case ExporterOCI:
82+
case ExporterOCI, ExporterDocker:
8383
outputFile, ok := opt.ExporterAttrs[exporterOCIDestination]
8484
if ok {
8585
fi, err := os.Stat(outputFile)
@@ -91,7 +91,7 @@ func (c *Client) Solve(ctx context.Context, def *llb.Definition, opt SolveOpt, s
9191
}
9292
} else {
9393
if _, err := console.ConsoleFromFile(os.Stdout); err == nil {
94-
return errors.Errorf("output file is required for OCI exporter. refusing to write to console")
94+
return errors.Errorf("output file is required for %s exporter. refusing to write to console", opt.Exporter)
9595
}
9696
outputFile = ""
9797
}

0 commit comments

Comments
 (0)