Skip to content

Commit bb2f5be

Browse files
committed
support using local image tarballs with FROM
This adds 'image args', configurable with IMAGE_ARG_* - similar to BUILD_ARG_*, only the value points to an image tarball. The image tarball will be loaded and served by a local registry, and a reference to the image in the local registry will be provided as the build arg. To use this, you must modify your Dockerfile like so: ARG base_image=ubuntu FROM ${base_image} Then, when running oci-build-task, specify: params: IMAGE_ARG_base_image: ubuntu/image.tar This will remain forward compatible if we ever switch to Kaniko (#46), which would also require using build args as Kaniko image caching requires a full digest to be specified in FROM. fixes #1 closes #2 closes #3 closes #14 Signed-off-by: Alex Suraci <[email protected]>
1 parent 25121ea commit bb2f5be

File tree

9 files changed

+430
-11
lines changed

9 files changed

+430
-11
lines changed

README.md

+6
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ Next, any of the following optional parameters may be specified:
6969
DO_THING=false
7070
```
7171
72+
* `IMAGE_ARG_*`: params prefixed with `IMAGE_ARG_*` point to image tarballs
73+
(i.e. `docker save` format) to preload so that they do not have to be fetched
74+
during the build. An image reference will be provided as the given build arg
75+
name. For example, `IMAGE_ARG_base_image=ubuntu/image.tar` will set
76+
`base_image` to a local image reference for using `ubuntu/image.tar`.
77+
7278
* `$LABEL_*`: params prefixed with `LABEL_` will be set as image labels.
7379
For example `LABEL_foo=bar`, will set the `foo` label to `bar`.
7480

cmd/build/main.go

+9
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
)
1414

1515
const buildArgPrefix = "BUILD_ARG_"
16+
const imageArgPrefix = "IMAGE_ARG_"
1617
const labelPrefix = "LABEL_"
1718

1819
func main() {
@@ -31,6 +32,14 @@ func main() {
3132
strings.TrimPrefix(env, buildArgPrefix),
3233
)
3334
}
35+
36+
if strings.HasPrefix(env, imageArgPrefix) {
37+
req.Config.ImageArgs = append(
38+
req.Config.ImageArgs,
39+
strings.TrimPrefix(env, imageArgPrefix),
40+
)
41+
}
42+
3443
if strings.HasPrefix(env, labelPrefix) {
3544
req.Config.Labels = append(
3645
req.Config.Labels,

go.mod

+11-4
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,32 @@ go 1.12
44

55
require (
66
github.com/BurntSushi/toml v0.3.1
7-
github.com/VividCortex/ewma v1.1.1 // indirect
87
github.com/concourse/go-archive v1.0.1
9-
github.com/containerd/stargz-snapshotter/estargz v0.0.0-20210104002936-0eb1adb9f9f7 // indirect
8+
github.com/containerd/stargz-snapshotter/estargz v0.0.0-20210105085455-7f45f7438617 // indirect
9+
github.com/containers/image/v5 v5.9.0
10+
github.com/docker/cli v20.10.2+incompatible // indirect
11+
github.com/docker/docker v20.10.2+incompatible // indirect
1012
github.com/fatih/color v1.10.0
1113
github.com/golang/protobuf v1.4.3 // indirect
1214
github.com/google/go-cmp v0.5.2 // indirect
1315
github.com/google/go-containerregistry v0.3.0
16+
github.com/julienschmidt/httprouter v1.3.0
17+
github.com/klauspost/compress v1.11.6 // indirect
1418
github.com/onsi/gomega v1.10.3 // indirect
19+
github.com/opencontainers/go-digest v1.0.0
1520
github.com/pkg/errors v0.9.1
1621
github.com/sirupsen/logrus v1.7.0
1722
github.com/stretchr/testify v1.6.1
1823
github.com/u-root/u-root v7.0.0+incompatible
1924
github.com/vbauerster/mpb v3.4.0+incompatible
2025
github.com/vrischmann/envconfig v1.3.0
2126
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect
22-
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b // indirect
23-
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad // indirect
27+
golang.org/x/net v0.0.0-20201224014010-6772e930b67b // indirect
28+
golang.org/x/sys v0.0.0-20210108172913-0df2131ae363 // indirect
2429
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect
2530
golang.org/x/text v0.3.4 // indirect
2631
google.golang.org/protobuf v1.25.0 // indirect
2732
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect
33+
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
34+
gotest.tools/v3 v3.0.3 // indirect
2835
)

go.sum

+169-6
Large diffs are not rendered by default.

registry.go

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package task
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"net"
8+
"net/http"
9+
10+
"github.com/containers/image/v5/docker/archive"
11+
"github.com/containers/image/v5/types"
12+
"github.com/julienschmidt/httprouter"
13+
"github.com/opencontainers/go-digest"
14+
"github.com/sirupsen/logrus"
15+
)
16+
17+
type LocalRegistry map[string]types.ImageSource
18+
19+
func LoadRegistry(imagePaths map[string]string) (LocalRegistry, error) {
20+
images := LocalRegistry{}
21+
for name, path := range imagePaths {
22+
ref, err := archive.NewReference(path, nil)
23+
if err != nil {
24+
return nil, fmt.Errorf("new reference: %w", err)
25+
}
26+
27+
src, err := ref.NewImageSource(context.TODO(), nil)
28+
if err != nil {
29+
return nil, fmt.Errorf("new image source: %w", err)
30+
}
31+
32+
images[name] = src
33+
}
34+
35+
return images, nil
36+
}
37+
38+
func ServeRegistry(reg LocalRegistry) (string, error) {
39+
router := httprouter.New()
40+
router.GET("/v2/:name/manifests/:ignored", reg.GetManifest)
41+
router.GET("/v2/:name/blobs/:digest", reg.GetBlob)
42+
43+
router.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
44+
logrus.WithFields(logrus.Fields{
45+
"method": r.Method,
46+
"path": r.URL.Path,
47+
}).Warnf("unknown request")
48+
})
49+
50+
listener, err := net.Listen("tcp", ":0")
51+
if err != nil {
52+
return "", fmt.Errorf("listen: %w", err)
53+
}
54+
55+
go http.Serve(listener, router)
56+
57+
_, port, err := net.SplitHostPort(listener.Addr().String())
58+
if err != nil {
59+
return "", fmt.Errorf("split registry host/port: %w", err)
60+
}
61+
62+
return port, nil
63+
}
64+
65+
func (registry LocalRegistry) BuildArgs(port string) []string {
66+
var buildArgs []string
67+
for name := range registry {
68+
buildArgs = append(buildArgs, fmt.Sprintf("%s=localhost:%s/%s", name, port, name))
69+
}
70+
71+
return buildArgs
72+
}
73+
74+
func (registry LocalRegistry) GetManifest(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
75+
name := p.ByName("name")
76+
77+
src, found := registry[name]
78+
if !found {
79+
w.WriteHeader(http.StatusNotFound)
80+
return
81+
}
82+
83+
blob, mt, err := src.GetManifest(r.Context(), nil)
84+
if err != nil {
85+
logrus.Errorf("failed to get manifest: %s", err)
86+
w.WriteHeader(http.StatusInternalServerError)
87+
return
88+
}
89+
90+
w.Header().Set("Content-Type", mt)
91+
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(blob)))
92+
w.Header().Set("Docker-Content-Digest", digest.FromBytes(blob).String())
93+
94+
if r.Method == "HEAD" {
95+
return
96+
}
97+
98+
_, err = w.Write(blob)
99+
if err != nil {
100+
logrus.Errorf("write manifest blob: %s", err)
101+
return
102+
}
103+
}
104+
105+
func (registry LocalRegistry) GetBlob(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
106+
name := p.ByName("name")
107+
108+
src, found := registry[name]
109+
if !found {
110+
w.WriteHeader(http.StatusNotFound)
111+
return
112+
}
113+
114+
blob, size, err := src.GetBlob(r.Context(), types.BlobInfo{
115+
Digest: digest.Digest(p.ByName("digest")),
116+
}, nil)
117+
if err != nil {
118+
logrus.Errorf("failed to get blob: %s", err)
119+
w.WriteHeader(http.StatusInternalServerError)
120+
return
121+
}
122+
123+
w.Header().Set("Content-Length", fmt.Sprintf("%d", size))
124+
125+
if r.Method == "HEAD" {
126+
return
127+
}
128+
129+
_, err = io.Copy(w, blob)
130+
if err != nil {
131+
logrus.Errorf("write blob: %s", err)
132+
return
133+
}
134+
}

task.go

+24
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,30 @@ func Build(buildkitd *Buildkitd, outputsDir string, req Request) (Response, erro
5757
)
5858
}
5959

60+
if len(req.Config.ImageArgs) > 0 {
61+
imagePaths := map[string]string{}
62+
for _, arg := range req.Config.ImageArgs {
63+
segs := strings.SplitN(arg, "=", 2)
64+
imagePaths[segs[0]] = segs[1]
65+
}
66+
67+
registry, err := LoadRegistry(imagePaths)
68+
if err != nil {
69+
return Response{}, fmt.Errorf("create local image registry: %w", err)
70+
}
71+
72+
port, err := ServeRegistry(registry)
73+
if err != nil {
74+
return Response{}, fmt.Errorf("create local image registry: %w", err)
75+
}
76+
77+
for _, arg := range registry.BuildArgs(port) {
78+
buildctlArgs = append(buildctlArgs,
79+
"--opt", "build-arg:"+arg,
80+
)
81+
}
82+
}
83+
6084
if _, err := os.Stat(cacheDir); err == nil {
6185
buildctlArgs = append(buildctlArgs,
6286
"--export-cache", "type=local,mode=min,dest="+cacheDir,

task_test.go

+62-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
"github.com/google/go-containerregistry/pkg/name"
1515
"github.com/google/go-containerregistry/pkg/registry"
16+
v1 "github.com/google/go-containerregistry/pkg/v1"
1617
"github.com/google/go-containerregistry/pkg/v1/random"
1718
"github.com/google/go-containerregistry/pkg/v1/remote"
1819
"github.com/google/go-containerregistry/pkg/v1/tarball"
@@ -281,7 +282,7 @@ func (s *TaskSuite) TestRegistryMirrors() {
281282

282283
builtLayers, err := builtImage.Layers()
283284
s.NoError(err)
284-
s.Len(layers, len(layers))
285+
s.Len(builtLayers, len(layers))
285286

286287
for i := 0; i < len(layers); i++ {
287288
digest, err := layers[i].Digest()
@@ -294,6 +295,66 @@ func (s *TaskSuite) TestRegistryMirrors() {
294295
}
295296
}
296297

298+
func (s *TaskSuite) TestImageArgs() {
299+
imagesDir, err := ioutil.TempDir("", "preload-images")
300+
s.NoError(err)
301+
302+
defer os.RemoveAll(imagesDir)
303+
304+
firstImage, err := random.Image(1024, 2)
305+
s.NoError(err)
306+
firstPath := filepath.Join(imagesDir, "first.tar")
307+
err = tarball.WriteToFile(firstPath, nil, firstImage)
308+
s.NoError(err)
309+
310+
secondImage, err := random.Image(1024, 2)
311+
s.NoError(err)
312+
secondPath := filepath.Join(imagesDir, "second.tar")
313+
err = tarball.WriteToFile(secondPath, nil, secondImage)
314+
s.NoError(err)
315+
316+
s.req.Config.ContextDir = "testdata/image-args"
317+
s.req.Config.AdditionalTargets = []string{"first"}
318+
s.req.Config.ImageArgs = []string{
319+
"first_image=" + firstPath,
320+
"second_image=" + secondPath,
321+
}
322+
323+
err = os.Mkdir(s.outputPath("first"), 0755)
324+
s.NoError(err)
325+
326+
_, err = s.build()
327+
s.NoError(err)
328+
329+
firstBuiltImage, err := tarball.ImageFromPath(s.outputPath("first", "image.tar"), nil)
330+
s.NoError(err)
331+
332+
secondBuiltImage, err := tarball.ImageFromPath(s.outputPath("image", "image.tar"), nil)
333+
s.NoError(err)
334+
335+
for image, builtImage := range map[v1.Image]v1.Image{
336+
firstImage: firstBuiltImage,
337+
secondImage: secondBuiltImage,
338+
} {
339+
layers, err := image.Layers()
340+
s.NoError(err)
341+
342+
builtLayers, err := builtImage.Layers()
343+
s.NoError(err)
344+
s.Len(builtLayers, len(layers)+1)
345+
346+
for i := 0; i < len(layers); i++ {
347+
digest, err := layers[i].Digest()
348+
s.NoError(err)
349+
350+
builtDigest, err := builtLayers[i].Digest()
351+
s.NoError(err)
352+
353+
s.Equal(digest, builtDigest)
354+
}
355+
}
356+
}
357+
297358
func (s *TaskSuite) TestMultiTarget() {
298359
s.req.Config.ContextDir = "testdata/multi-target"
299360
s.req.Config.AdditionalTargets = []string{"additional-target"}

testdata/image-args/Dockerfile

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
ARG first_image
2+
ARG second_image
3+
4+
FROM ${first_image} AS first
5+
COPY Dockerfile /Dockerfile.first
6+
7+
FROM ${second_image}
8+
COPY Dockerfile /Dockerfile.second

types.go

+7
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,13 @@ type Config struct {
6565
//
6666
// Theoretically this would go away if/when we standardize on OCI.
6767
UnpackRootfs bool `json:"unpack_rootfs" envconfig:"optional"`
68+
69+
// Images to pre-load in order to avoid fetching at build time. Mapping from
70+
// build arg name to OCI image tarball path.
71+
//
72+
// Each image will be pre-loaded and a build arg will be set to a value
73+
// appropriate for setting in 'FROM ...'.
74+
ImageArgs []string `json:"image_args" envconfig:"optional"`
6875
}
6976

7077
// ImageMetadata is the schema written to manifest.json when producing the

0 commit comments

Comments
 (0)