Skip to content

Commit 34cca18

Browse files
committed
Support json mode transfer
1 parent fbb4645 commit 34cca18

3 files changed

Lines changed: 109 additions & 15 deletions

File tree

pkg/build/buildopts.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ const (
6565
KeySecrets = "secrets"
6666
// SSH agent to forward during the build process.
6767
KeySSH = "ssh"
68+
// Build context transfer mode: "tar" (default) or "json" (metadata-only, content fetched on demand).
69+
KeyTransferMode = "transfer-mode"
6870
// Cache import sources.
6971
KeyCacheIn = "cache-in"
7072
// Cache export destinations.
@@ -277,6 +279,7 @@ func NewBuildOpts(ctx context.Context, basePath string, contextMap map[string][]
277279
return nil, err
278280
}
279281
ssh := sshExtract(KeySSH)
282+
transferMode, _ := first(KeyTransferMode)
280283
cacheIn := contextMap[KeyCacheIn]
281284
cacheOut := contextMap[KeyCacheOut]
282285
outputs := contextMap[KeyOutput]
@@ -350,7 +353,7 @@ func NewBuildOpts(ctx context.Context, basePath string, contextMap map[string][]
350353
}
351354
}
352355

353-
fssyncProxy, err := fssync.NewFSSyncProxy(".", basePath, addedGlobs, dockerfileBytes, dockerignoreBytes)
356+
fssyncProxy, err := fssync.NewFSSyncProxy(".", basePath, addedGlobs, dockerfileBytes, dockerignoreBytes, transferMode)
354357
if err != nil {
355358
return nil, err
356359
}

pkg/fssync/fssync.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,12 @@ type FSSyncProxy struct {
5858

5959
dockerfile []byte
6060
dockerignore []byte
61+
62+
mode TransferMode
6163
}
6264

6365
func NewFSSyncProxy(contextDir string, basePath string, addedGlobs []string,
64-
dockerfile []byte, dockerignore []byte) (*FSSyncProxy, error) {
66+
dockerfile []byte, dockerignore []byte, mode string) (*FSSyncProxy, error) {
6567

6668
f := new(FSSyncProxy)
6769
f.contextDir = contextDir
@@ -70,6 +72,13 @@ func NewFSSyncProxy(contextDir string, basePath string, addedGlobs []string,
7072

7173
f.dockerfile = dockerfile
7274
f.dockerignore = dockerignore
75+
76+
switch TransferMode(mode) {
77+
case ModeJSON:
78+
f.mode = ModeJSON
79+
default:
80+
f.mode = ModeTAR
81+
}
7382
return f, nil
7483
}
7584

pkg/fssync/walk.go

Lines changed: 95 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ package fssync
1818

1919
import (
2020
"context"
21+
"encoding/json"
2122
"fmt"
2223
"io/fs"
2324
"strings"
25+
"time"
2426

2527
"github.com/google/uuid"
2628
"github.com/moby/patternmatcher"
@@ -90,7 +92,7 @@ func (f *FS) Walk(ctx context.Context, target string, fn fs.WalkDirFunc) error {
9092
cancellableCtx, cancel := context.WithCancel(ctx)
9193
defer cancel()
9294

93-
walkMeta, err := unmarshalWalkMetadata(cancellableCtx)
95+
walkMeta, err := unmarshalWalkMetadata(cancellableCtx, f.proxy.mode)
9496
if err != nil {
9597
return err
9698
}
@@ -150,11 +152,101 @@ func (f *FS) Walk(ctx context.Context, target string, fn fs.WalkDirFunc) error {
150152
defer f._checksumMutex.Unlock()
151153
f._checksum = checksum
152154
return nil
155+
case ModeJSON:
156+
return receiveJSON(demux, excludeMatcher, f.proxy.dockerfile, f.proxy.dockerignore, fn)
153157
default:
154158
return fmt.Errorf("unsupported walk mode: %q", walkMeta.Mode)
155159
}
156160
}
157161

162+
// receiveJSON handles ModeJSON walk responses. The proxy sends a single
163+
// BuildTransfer whose Data field is a JSON array of RawFileInfo. File contents
164+
// are not transferred here; they are fetched on-demand via FS.Open.
165+
func receiveJSON(demux *stream.Demultiplexer, excludeMatcher *patternmatcher.PatternMatcher, dockerfile, dockerignore []byte, fn fs.WalkDirFunc) error {
166+
resp, err := demux.Recv()
167+
if err != nil {
168+
return fmt.Errorf("json walk: failed receiving response: %w", err)
169+
}
170+
bt := resp.GetBuildTransfer()
171+
if bt == nil {
172+
return fmt.Errorf("json walk: expected BuildTransfer, got nil")
173+
}
174+
if errMsg, ok := bt.Metadata["error"]; ok {
175+
return fmt.Errorf("json walk: server error: %s", errMsg)
176+
}
177+
178+
var files []RawFileInfo
179+
if err := json.Unmarshal(bt.Data, &files); err != nil {
180+
return fmt.Errorf("json walk: failed to unmarshal file list: %w", err)
181+
}
182+
183+
// Staged Dockerfile/dockerignore live under DockerfileStaging (".com.apple.container").
184+
// That prefix starts with '.' which sorts before any regular path component,
185+
// so these entries must be emitted BEFORE the regular file list.
186+
if len(dockerignore) > 0 {
187+
stagingDir := DockerfileStaging
188+
dirEntry := &fileutils.FileInfo{
189+
NameVal: stagingDir,
190+
ModeVal: fs.ModeDir | 0755,
191+
IsDirVal: true,
192+
}
193+
if err := fn(stagingDir, fs.FileInfoToDirEntry(dirEntry), nil); err != nil {
194+
return err
195+
}
196+
for _, staged := range []struct {
197+
name string
198+
data []byte
199+
}{
200+
{"Dockerfile", dockerfile},
201+
{"Dockerfile.dockerignore", dockerignore},
202+
} {
203+
path := stagingDir + "/" + staged.name
204+
fi := &fileutils.FileInfo{
205+
NameVal: path,
206+
SizeVal: int64(len(staged.data)),
207+
ModeVal: 0644,
208+
}
209+
if err := fn(path, fs.FileInfoToDirEntry(fi), nil); err != nil {
210+
return err
211+
}
212+
}
213+
}
214+
215+
for _, f := range files {
216+
excluded, err := excludeMatcher.MatchesOrParentMatches(f.Name)
217+
if err != nil {
218+
return err
219+
}
220+
if excluded {
221+
continue
222+
}
223+
modTime, err := time.Parse(time.RFC3339, f.ModTime)
224+
if err != nil {
225+
modTime = time.Time{}
226+
}
227+
modeVal := fs.FileMode(f.Mode)
228+
if f.IsDir {
229+
modeVal |= fs.ModeDir
230+
} else if f.Target != "" {
231+
modeVal |= fs.ModeSymlink
232+
}
233+
fi := &fileutils.FileInfo{
234+
NameVal: f.Name,
235+
SizeVal: int64(f.Size),
236+
ModeVal: modeVal,
237+
ModTimeVal: modTime,
238+
IsDirVal: f.IsDir,
239+
Uid: f.UID,
240+
Gid: f.GID,
241+
LinkName: f.Target,
242+
}
243+
if err := fn(f.Name, fs.FileInfoToDirEntry(fi), nil); err != nil {
244+
return err
245+
}
246+
}
247+
return nil
248+
}
249+
158250
// RawFileInfo is the wire‑format for Walk (json mode).
159251
type RawFileInfo struct {
160252
Name string `json:"name"`
@@ -175,23 +267,13 @@ type WalkMetadata struct {
175267
Mode TransferMode
176268
}
177269

178-
func unmarshalWalkMetadata(ctx context.Context) (*WalkMetadata, error) {
179-
md := &WalkMetadata{}
270+
func unmarshalWalkMetadata(ctx context.Context, mode TransferMode) (*WalkMetadata, error) {
271+
md := &WalkMetadata{Mode: mode}
180272
if m, ok := metadata.FromIncomingContext(ctx); ok {
181273
md.IncludePatterns = strings.Join(m["include-patterns"], ",")
182274
md.ExcludedPatterns = strings.Join(m["exclude-patterns"], ",")
183275
md.FollowPaths = strings.Join(m["followpaths"], ",")
184276
md.DirName = strings.Join(m["dir-name"], ",")
185-
modeStr := strings.Join(m["mode"], ",")
186-
switch modeStr {
187-
case "", "tar":
188-
modeStr = string(ModeTAR)
189-
default:
190-
return nil, fmt.Errorf("invalid walk mode: %s", modeStr)
191-
}
192-
md.Mode = TransferMode(modeStr)
193-
} else {
194-
md.Mode = ModeTAR
195277
}
196278
return md, nil
197279
}

0 commit comments

Comments
 (0)