Skip to content

Commit 21b96b5

Browse files
committed
Make docker builds lazy-write tarball
1 parent f407a63 commit 21b96b5

File tree

7 files changed

+740
-83
lines changed

7 files changed

+740
-83
lines changed

pkg/dockerbuild/dockerbuild.go

Lines changed: 12 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package dockerbuild
22

33
import (
4-
"archive/tar"
5-
"bytes"
64
"context"
75
"encoding/json"
86
"io"
@@ -154,18 +152,7 @@ func resolveBaseImage(ctx context.Context, baseImgTag string, overrideBaseImage
154152
}
155153

156154
func buildImageFilesystem(ctx context.Context, spec *ImageSpec, cfg *ImageBuildConfig) (opener tarball.Opener, err error) {
157-
tarFile, err := os.CreateTemp("", "docker-img")
158-
if err != nil {
159-
return nil, errors.Wrap(err, "mktemp")
160-
}
161-
defer func() {
162-
if e := tarFile.Close(); e != nil && err == nil {
163-
err = errors.Wrap(e, "close docker-img file")
164-
}
165-
}()
166-
167-
tw := tar.NewWriter(tarFile)
168-
tc := newTarCopier(tw, setFileTimes(cfg.BuildTime))
155+
tc := newTarCopier(setFileTimes(cfg.BuildTime))
169156

170157
// Bundle the source code, if requested.
171158
if bundle, ok := spec.BundleSource.Get(); ok {
@@ -199,24 +186,17 @@ func buildImageFilesystem(ctx context.Context, spec *ImageSpec, cfg *ImageBuildC
199186
if caCertsDest == "" {
200187
caCertsDest = DefaultCACertsPath
201188
}
202-
if err := addCACerts(ctx, tw, caCertsDest); err != nil {
189+
if err := addCACerts(ctx, tc, caCertsDest); err != nil {
203190
return nil, errors.Wrap(err, "add ca certs")
204191
}
205192
}
206193

207-
if err := tw.Close(); err != nil {
208-
return nil, errors.Wrap(err, "complete tar")
209-
}
210-
211-
opener = func() (io.ReadCloser, error) {
212-
return os.Open(tarFile.Name())
213-
}
214-
return opener, nil
194+
return tc.Opener(), nil
215195
}
216196

217197
func writeExtraFiles(tc *tarCopier, files map[ImagePath][]byte) error {
218198
for path, data := range files {
219-
if err := tc.WriteFile(path.String(), 0644, data); err != nil {
199+
if err := tc.WriteFile(path, 0644, data); err != nil {
220200
return errors.Wrap(err, "write image data")
221201
}
222202
}
@@ -254,7 +234,7 @@ func setupSupervisor(tc *tarCopier, spec *ImageSpec, cfg *ImageBuildConfig) erro
254234
if err != nil {
255235
return errors.Wrap(err, "marshal supervisor config")
256236
}
257-
if err := tc.WriteFile(string(super.ConfigPath), 0644, data); err != nil {
237+
if err := tc.WriteFile(super.ConfigPath, 0644, data); err != nil {
258238
return errors.Wrap(err, "write supervisor config")
259239
}
260240
}
@@ -290,7 +270,7 @@ func writeBuildInfo(tc *tarCopier, spec BuildInfoSpec) error {
290270
return errors.Wrap(err, "marshal build info")
291271
}
292272

293-
err = tc.WriteFile(string(spec.InfoPath), 0644, info)
273+
err = tc.WriteFile(spec.InfoPath, 0644, info)
294274
return errors.Wrap(err, "write build info")
295275
}
296276

@@ -312,7 +292,7 @@ func tryFetch(ctx context.Context, url string) (*http.Response, error) {
312292
return resp, nil
313293
}
314294

315-
func addCACerts(ctx context.Context, tw *tar.Writer, dest ImagePath) error {
295+
func addCACerts(ctx context.Context, tc *tarCopier, dest ImagePath) error {
316296
const (
317297
encoreCachedRootCerts = "https://api.encore.cloud/artifacts/build/root-certs"
318298
curlCACertStore = "https://curl.se/ca/cacert.pem"
@@ -333,34 +313,14 @@ func addCACerts(ctx context.Context, tw *tar.Writer, dest ImagePath) error {
333313
}
334314
defer func() { _ = resp.Body.Close() }()
335315

336-
// We need to populate the body of the tar file before writing the contents.
337-
// Use the content length if it was provided. Otherwise, read the whole response
338-
// into memory and use its length.
339-
var body io.Reader = resp.Body
340-
size := resp.ContentLength
341-
if size < 0 {
342-
// Unknown body; read the whole response into memory
343-
data, err := io.ReadAll(resp.Body)
344-
if err != nil {
345-
return errors.Wrap(err, "read cert data")
346-
}
347-
size = int64(len(data))
348-
body = bytes.NewReader(data)
316+
data, err := io.ReadAll(resp.Body)
317+
if err != nil {
318+
return errors.Wrap(err, "read cert data")
349319
}
350320

351321
// Add the file
352-
err = tw.WriteHeader(&tar.Header{
353-
Typeflag: tar.TypeReg,
354-
Name: string(dest),
355-
Size: size,
356-
})
357-
if err != nil {
358-
return errors.Wrap(err, "create cert file")
359-
}
360-
if _, err := io.Copy(tw, body); err != nil {
361-
return errors.Wrap(err, "write cert data")
362-
}
363-
return nil
322+
err = tc.WriteFile(dest, 0644, data)
323+
return err
364324
}
365325

366326
type envMap map[string]string

pkg/dockerbuild/tarcopy.go

Lines changed: 96 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package dockerbuild
22

33
import (
44
"archive/tar"
5+
"bytes"
6+
"fmt"
57
"io"
68
"io/fs"
79
"os"
@@ -11,21 +13,24 @@ import (
1113
"strings"
1214
"time"
1315

14-
"encr.dev/pkg/xos"
15-
"encr.dev/v2/compiler/build"
1616
"github.com/cockroachdb/errors"
17+
"github.com/google/go-containerregistry/pkg/v1/tarball"
1718
"github.com/rs/zerolog/log"
19+
20+
"encr.dev/pkg/option"
21+
"encr.dev/pkg/tarstream"
22+
"encr.dev/pkg/xos"
23+
"encr.dev/v2/compiler/build"
1824
)
1925

2026
type tarCopier struct {
2127
fileTimes *time.Time
22-
tw *tar.Writer
28+
entries []*tarEntry
2329
seenDirs map[ImagePath]bool
2430
}
2531

26-
func newTarCopier(tw *tar.Writer, opts ...tarCopyOption) *tarCopier {
32+
func newTarCopier(opts ...tarCopyOption) *tarCopier {
2733
tc := &tarCopier{
28-
tw: tw,
2934
seenDirs: make(map[ImagePath]bool),
3035
}
3136
for _, opt := range opts {
@@ -263,9 +268,9 @@ func (tc *tarCopier) MkdirAll(dstPath ImagePath, mode fs.FileMode) (err error) {
263268
Name: (dstPath + "/").String(), // from [archive/tar.FileInfoHeader]
264269
Mode: int64(mode.Perm()),
265270
}
266-
if err := tc.tw.WriteHeader(header); err != nil {
267-
return errors.Wrap(err, "write tar header")
268-
}
271+
tc.entries = append(tc.entries, &tarEntry{
272+
header: header,
273+
})
269274
tc.seenDirs[dstPath] = true
270275
}
271276

@@ -293,9 +298,10 @@ func (tc *tarCopier) CopyFile(dstPath ImagePath, srcPath HostPath, fi fs.FileInf
293298
}
294299

295300
header.Name = filepath.ToSlash(dstPath.String())
296-
if err := tc.tw.WriteHeader(header); err != nil {
297-
return errors.Wrap(err, "write tar header")
301+
entry := &tarEntry{
302+
header: header,
298303
}
304+
tc.entries = append(tc.entries, entry)
299305

300306
if fi.IsDir() {
301307
tc.seenDirs[dstPath] = true
@@ -304,28 +310,15 @@ func (tc *tarCopier) CopyFile(dstPath ImagePath, srcPath HostPath, fi fs.FileInf
304310

305311
// If this is not a symlink, write the file.
306312
if (fi.Mode() & fs.ModeSymlink) != fs.ModeSymlink {
307-
// Write the file
308-
f, err := os.Open(srcPath.String())
309-
if err != nil {
310-
return errors.Wrap(err, "open file")
311-
}
312-
defer func() {
313-
if closeErr := f.Close(); err == nil {
314-
err = errors.Wrap(closeErr, "close file")
315-
}
316-
}()
317-
318-
if _, err = io.Copy(tc.tw, f); err != nil {
319-
return errors.Wrap(err, "copy file")
320-
}
313+
entry.hostPath = option.Some(srcPath)
321314
}
322315

323316
return nil
324317
}
325318

326-
func (tc *tarCopier) WriteFile(dstPath string, mode fs.FileMode, data []byte) (err error) {
319+
func (tc *tarCopier) WriteFile(dstPath ImagePath, mode fs.FileMode, data []byte) (err error) {
327320
header := &tar.Header{
328-
Name: dstPath,
321+
Name: filepath.ToSlash(dstPath.String()),
329322
Typeflag: tar.TypeReg,
330323
Mode: int64(mode.Perm()),
331324
Size: int64(len(data)),
@@ -337,11 +330,83 @@ func (tc *tarCopier) WriteFile(dstPath string, mode fs.FileMode, data []byte) (e
337330
header.ChangeTime = t
338331
}
339332

340-
header.Name = filepath.ToSlash(dstPath)
341-
if err := tc.tw.WriteHeader(header); err != nil {
342-
return errors.Wrap(err, "write tar header")
333+
tc.entries = append(tc.entries, &tarEntry{
334+
header: header,
335+
data: option.Some(data),
336+
})
337+
return nil
338+
}
339+
340+
type tarEntry struct {
341+
header *tar.Header
342+
343+
data option.Option[[]byte]
344+
hostPath option.Option[HostPath]
345+
}
346+
347+
func (tc *tarCopier) Opener() tarball.Opener {
348+
errThunk := func(err error) tarball.Opener {
349+
return func() (io.ReadCloser, error) {
350+
return nil, err
351+
}
343352
}
344353

345-
_, err = tc.tw.Write(data)
346-
return errors.Wrap(err, "write file")
354+
var tv tarstream.TarVec
355+
for _, e := range tc.entries {
356+
// create buffer to write tar header to
357+
buf := new(bytes.Buffer)
358+
tw := tar.NewWriter(buf)
359+
360+
// write tar header to buffer
361+
if err := tw.WriteHeader(e.header); err != nil {
362+
return errThunk(errors.Wrap(err, fmt.Sprintf("writing header %v", e)))
363+
}
364+
365+
memv := tarstream.MemVec{
366+
Data: buf.Bytes(),
367+
}
368+
369+
// add the tar header mem buffer to the tarvec
370+
tv.Dvecs = append(tv.Dvecs, memv)
371+
tv.Size += memv.GetSize()
372+
373+
var dataEntry tarstream.Datavec
374+
if hostPath, ok := e.hostPath.Get(); ok {
375+
fi := e.header.FileInfo()
376+
dataEntry = &tarstream.PathVec{
377+
Path: hostPath.String(),
378+
Info: fi,
379+
}
380+
} else if data, ok := e.data.Get(); ok {
381+
dataEntry = tarstream.MemVec{Data: data}
382+
}
383+
384+
if dataEntry != nil {
385+
// add the file path info to the tarvec
386+
size := dataEntry.GetSize()
387+
tv.Size += size
388+
tv.Dvecs = append(tv.Dvecs, dataEntry)
389+
390+
// tar requires file entries to be padded out to
391+
// 512 byte offset
392+
// if needed, record how much padding is needed
393+
// and add to the tarvec
394+
if size%512 != 0 {
395+
padv := tarstream.PadVec{
396+
Size: 512 - (size % 512),
397+
}
398+
399+
tv.Dvecs = append(tv.Dvecs, padv)
400+
tv.Size += padv.GetSize()
401+
}
402+
}
403+
}
404+
405+
tv.ComputeSize()
406+
tv.Pos = 0
407+
408+
return func() (io.ReadCloser, error) {
409+
tv2 := tv.Clone()
410+
return tv2, nil
411+
}
347412
}

pkg/tarstream/LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2016 Ben McClelland
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

0 commit comments

Comments
 (0)