Skip to content

Commit 8145df0

Browse files
committed
feat: add build-context support.
1 parent 267b5ab commit 8145df0

5 files changed

Lines changed: 109 additions & 34 deletions

File tree

pkg/build/build.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,19 @@ func Build(ctx context.Context, opts *BOpts) error {
171171
for k, v := range opts.Labels {
172172
solveOpt.FrontendAttrs["label:"+k] = v
173173
}
174+
for name, ref := range opts.BuildContexts {
175+
switch strings.SplitN(ref, ":", 2)[0] {
176+
case "docker-image", "git", "http", "https":
177+
solveOpt.FrontendAttrs["context:"+name] = ref
178+
case "oci-layout":
179+
// oci-layout requires custom handling as it needs to load the layout data from the client
180+
// not setting solveOpt.FrontendAttrs["context:"+name] here for the frontend can handle it because namedcontext will resolve
181+
solveOpt.OCIStores[name] = opts.ContentStore
182+
default: // bare path → local context
183+
solveOpt.FrontendAttrs["context:"+name] = "local:" + name
184+
}
185+
}
186+
174187
solveOpt.Frontend = "dockerfile.v1"
175188

176189
if len(opts.SSH) > 0 {

pkg/build/buildopts.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ const (
5353
KeyProgress = "progress"
5454
// When present, disables layer caching.
5555
KeyNoCache = "no-cache"
56-
// Build context directory path.
57-
KeyContext = "context"
56+
// Build base context directory path.
57+
KeyContextDirectory = "context"
5858
// Dockerfile stage to build up to.
5959
KeyTarget = "target"
6060
// Key=value metadata labels to apply to the image.
@@ -73,6 +73,8 @@ const (
7373
KeyOutput = "outputs"
7474
// Unique build identifier.
7575
KeyBuildID = "build-id"
76+
// Additional Build contexts (--build-context).
77+
KeyBuildContext = "build-context"
7678
)
7779

7880
const (
@@ -103,6 +105,7 @@ type BOpts struct {
103105
Outputs []string
104106
Labels map[string]string
105107
ProgressWriter progresswriter.Writer
108+
BuildContexts map[string]string
106109

107110
ContentStore *content.ContentStoreProxy
108111
Resolver *resolver.ResolverProxy
@@ -169,7 +172,7 @@ func NewBuildOpts(ctx context.Context, basePath string, contextMap map[string][]
169172
}
170173

171174
ctxDir := "."
172-
if c, ok := first(KeyContext); ok {
175+
if c, ok := first(KeyContextDirectory); ok {
173176
ctxDir = c
174177
}
175178

@@ -272,6 +275,7 @@ func NewBuildOpts(ctx context.Context, basePath string, contextMap map[string][]
272275

273276
labels := mapExtract(KeyLabels)
274277
buildArgs := mapExtract(KeyBuildArgs)
278+
buildContexts := mapExtract(KeyBuildContext)
275279
secrets, err := mapExtractB64(KeySecrets)
276280
if err != nil {
277281
return nil, err
@@ -350,7 +354,7 @@ func NewBuildOpts(ctx context.Context, basePath string, contextMap map[string][]
350354
}
351355
}
352356

353-
fssyncProxy, err := fssync.NewFSSyncProxy(".", basePath, addedGlobs, dockerfileBytes, dockerignoreBytes)
357+
fssyncProxy, err := fssync.NewFSSyncProxy(ctxDir, basePath, addedGlobs, dockerfileBytes, dockerignoreBytes, buildContexts)
354358
if err != nil {
355359
return nil, err
356360
}
@@ -383,6 +387,7 @@ func NewBuildOpts(ctx context.Context, basePath string, contextMap map[string][]
383387
CacheOut: cacheOut,
384388
Outputs: outputs,
385389
basePath: filepath.Join(basePath, buildID),
390+
BuildContexts: buildContexts,
386391
}
387392

388393
return bopts, nil

pkg/build/frontend.go

Lines changed: 76 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"github.com/containerd/platforms"
3030
dref "github.com/distribution/reference"
3131

32+
"github.com/apple/container-builder-shim/pkg/build/utils"
3233
"github.com/moby/buildkit/client/llb"
3334
"github.com/moby/buildkit/client/llb/sourceresolver"
3435
"github.com/moby/buildkit/exporter/containerimage/exptypes"
@@ -42,8 +43,6 @@ import (
4243
"github.com/moby/buildkit/util/progress/progresswriter"
4344
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
4445
"github.com/sirupsen/logrus"
45-
46-
"github.com/apple/container-builder-shim/pkg/build/utils"
4746
)
4847

4948
func frontend(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
@@ -131,6 +130,79 @@ func resolveStates(ctx context.Context, bopts *BOpts, platform ocispecs.Platform
131130
stateLock := sync.Mutex{}
132131

133132
resolveSource := func(resolvedBaseStageName string, sourcePlatform ocispecs.Platform) error {
133+
134+
saveState := func(img []byte, fqdn string, ref string, storeName string, opts ...llb.OCILayoutOption) error {
135+
opts = append(opts, llb.Platform(sourcePlatform))
136+
opts = append(opts, llb.OCIStore("", storeName))
137+
138+
st := llb.OCILayout(fqdn, opts...)
139+
140+
st, err = st.WithImageConfig(img)
141+
if err != nil {
142+
return err
143+
}
144+
145+
named, err := dref.ParseNormalizedNamed(ref)
146+
if err != nil {
147+
return fmt.Errorf("invalid context name %s %v", ref, err)
148+
}
149+
// pname constructs a platform-qualified image reference in the format buildkit requires for digest resolution
150+
name := strings.TrimSuffix(dref.FamiliarString(named), ":latest")
151+
pname := name + "::" + platforms.FormatAll(platforms.Normalize(sourcePlatform))
152+
153+
imgMetaMap := map[string][]byte{
154+
exptypes.ExporterImageConfigKey: img,
155+
}
156+
imgMeta, err := json.Marshal(imgMetaMap)
157+
if err != nil {
158+
return err
159+
}
160+
161+
stateLock.Lock()
162+
states[pname] = stateMeta{
163+
state: st.Platform(sourcePlatform),
164+
imgMeta: imgMeta,
165+
}
166+
stateLock.Unlock()
167+
return nil
168+
}
169+
170+
// handle build context
171+
if val, ok := bopts.BuildContexts[resolvedBaseStageName]; ok {
172+
// oci-layout requires custom handling as namedContext cannot load from client correctly
173+
if strings.SplitN(val, ":", 2)[0] != "oci-layout" {
174+
return nil
175+
}
176+
// passing in "oci-layout" as they are so client side knows that it is oci from build-context
177+
resolverOpts := sourceresolver.Opt{}
178+
resolverOpts.ImageOpt = &sourceresolver.ResolveImageOpt{
179+
Platform: &sourcePlatform,
180+
ResolveMode: llb.ResolveModePreferLocal.String(),
181+
}
182+
resolverOpts.OCILayoutOpt = &sourceresolver.ResolveOCILayoutOpt{
183+
Store: sourceresolver.ResolveImageConfigOptStore{
184+
StoreID: resolvedBaseStageName,
185+
SessionID: "",
186+
},
187+
}
188+
_, digest, img, err := bopts.Resolver.ResolveImageConfig(ctx, val, resolverOpts)
189+
if err != nil {
190+
if err == reference.ErrObjectRequired {
191+
return nil
192+
}
193+
return err
194+
}
195+
196+
// not using the returning `ref` here as the `ref` will be the local path like /User/path/to/oci-layout
197+
// However, when using resolvedBaseStageName directly (ex: deps),
198+
// and if we don't add some dummy host like "docker.io/library/".
199+
// we will get an error like following
200+
// Error: unknown: "failed to solve: failed to load cache key: parse "dummy://deps@sha256:xxx": invalid port ":xxx" after host"
201+
ref := resolvedBaseStageName
202+
fqdn := "docker.io/library/" + ref + "@" + digest.String()
203+
return saveState(img, fqdn, ref, resolvedBaseStageName, llb.WithCustomName("[context "+resolvedBaseStageName+"] OCI load from client"))
204+
}
205+
134206
if strings.EqualFold(resolvedBaseStageName, "scratch") || strings.EqualFold(resolvedBaseStageName, "context") {
135207
return nil
136208
}
@@ -152,7 +224,7 @@ func resolveStates(ctx context.Context, bopts *BOpts, platform ocispecs.Platform
152224
}
153225
resolverOpts.OCILayoutOpt = &sourceresolver.ResolveOCILayoutOpt{
154226
Store: sourceresolver.ResolveImageConfigOptStore{
155-
StoreID: "container",
227+
StoreID: KeyContentStoreName,
156228
SessionID: "",
157229
},
158230
}
@@ -175,31 +247,7 @@ func resolveStates(ctx context.Context, bopts *BOpts, platform ocispecs.Platform
175247
if _, ok := ref.(dref.Digested); !ok {
176248
fqdn += "@" + digest.String()
177249
}
178-
st := llb.OCILayout(fqdn, llb.OCIStore("", "container"), llb.Platform(sourcePlatform))
179-
180-
named, err := dref.ParseNormalizedNamed(ref.String())
181-
if err != nil {
182-
return fmt.Errorf("invalid context name %s %v", ref.String(), err)
183-
}
184-
// pname constructs a platform-qualified image reference in the format buildkit requires for digest resolution
185-
name := strings.TrimSuffix(dref.FamiliarString(named), ":latest")
186-
pname := name + "::" + platforms.FormatAll(platforms.Normalize(sourcePlatform))
187-
188-
imgMetaMap := map[string][]byte{
189-
exptypes.ExporterImageConfigKey: img,
190-
}
191-
imgMeta, err := json.Marshal(imgMetaMap)
192-
if err != nil {
193-
return err
194-
}
195-
196-
stateLock.Lock()
197-
states[pname] = stateMeta{
198-
state: st.Platform(sourcePlatform),
199-
imgMeta: imgMeta,
200-
}
201-
stateLock.Unlock()
202-
return nil
250+
return saveState(img, fqdn, ref.String(), KeyContentStoreName)
203251
}
204252

205253
for i, stage := range stages {

pkg/fssync/fssync.go

Lines changed: 4 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+
buildContexts map[string]string
6163
}
6264

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

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

7173
f.dockerfile = dockerfile
7274
f.dockerignore = dockerignore
75+
f.buildContexts = buildContexts
7376
return f, nil
7477
}
7578

pkg/fssync/walk.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,17 @@ func (f *FS) Walk(ctx context.Context, target string, fn fs.WalkDirFunc) error {
107107
if followPaths == "" {
108108
followPaths = strings.Join(f.proxy.addedGlobs, ",")
109109
}
110+
root := f.root
111+
if walkMeta.DirName != "" && walkMeta.DirName != "context" && walkMeta.DirName != "scratch" {
112+
if val, ok := f.proxy.buildContexts[walkMeta.DirName]; ok {
113+
root = val
114+
}
115+
}
110116

111117
packet := &api.BuildTransfer{
112118
Id: id,
113119
Direction: api.TransferDirection_OUTOF,
114-
Source: &f.root,
120+
Source: &root,
115121
Metadata: map[string]string{
116122
"os": "linux",
117123
"stage": "fssync",

0 commit comments

Comments
 (0)