diff --git a/pkg/build/build.go b/pkg/build/build.go index be5be4c..8945edb 100644 --- a/pkg/build/build.go +++ b/pkg/build/build.go @@ -171,6 +171,19 @@ func Build(ctx context.Context, opts *BOpts) error { for k, v := range opts.Labels { solveOpt.FrontendAttrs["label:"+k] = v } + for name, ref := range opts.BuildContexts { + switch strings.SplitN(ref, ":", 2)[0] { + case "docker-image", "git", "http", "https": + solveOpt.FrontendAttrs["context:"+name] = ref + case "oci-layout": + // oci-layout requires custom handling as it needs to load the layout data from the client + // not setting solveOpt.FrontendAttrs["context:"+name] here for the frontend can handle it because namedcontext will resolve + solveOpt.OCIStores[name] = opts.ContentStore + default: // bare path → local context + solveOpt.FrontendAttrs["context:"+name] = "local:" + name + } + } + solveOpt.Frontend = "dockerfile.v1" if len(opts.SSH) > 0 { diff --git a/pkg/build/buildopts.go b/pkg/build/buildopts.go index fb77643..30ba3f5 100644 --- a/pkg/build/buildopts.go +++ b/pkg/build/buildopts.go @@ -53,8 +53,8 @@ const ( KeyProgress = "progress" // When present, disables layer caching. KeyNoCache = "no-cache" - // Build context directory path. - KeyContext = "context" + // Build base context directory path. + KeyContextDirectory = "context" // Dockerfile stage to build up to. KeyTarget = "target" // Key=value metadata labels to apply to the image. @@ -73,6 +73,8 @@ const ( KeyOutput = "outputs" // Unique build identifier. KeyBuildID = "build-id" + // Additional Build contexts (--build-context). + KeyBuildContext = "build-context" ) const ( @@ -103,6 +105,7 @@ type BOpts struct { Outputs []string Labels map[string]string ProgressWriter progresswriter.Writer + BuildContexts map[string]string ContentStore *content.ContentStoreProxy Resolver *resolver.ResolverProxy @@ -169,7 +172,7 @@ func NewBuildOpts(ctx context.Context, basePath string, contextMap map[string][] } ctxDir := "." - if c, ok := first(KeyContext); ok { + if c, ok := first(KeyContextDirectory); ok { ctxDir = c } @@ -272,6 +275,7 @@ func NewBuildOpts(ctx context.Context, basePath string, contextMap map[string][] labels := mapExtract(KeyLabels) buildArgs := mapExtract(KeyBuildArgs) + buildContexts := mapExtract(KeyBuildContext) secrets, err := mapExtractB64(KeySecrets) if err != nil { return nil, err @@ -350,7 +354,7 @@ func NewBuildOpts(ctx context.Context, basePath string, contextMap map[string][] } } - fssyncProxy, err := fssync.NewFSSyncProxy(".", basePath, addedGlobs, dockerfileBytes, dockerignoreBytes) + fssyncProxy, err := fssync.NewFSSyncProxy(ctxDir, basePath, addedGlobs, dockerfileBytes, dockerignoreBytes, buildContexts) if err != nil { return nil, err } @@ -383,6 +387,7 @@ func NewBuildOpts(ctx context.Context, basePath string, contextMap map[string][] CacheOut: cacheOut, Outputs: outputs, basePath: filepath.Join(basePath, buildID), + BuildContexts: buildContexts, } return bopts, nil diff --git a/pkg/build/frontend.go b/pkg/build/frontend.go index 94ebb07..b51b5d6 100644 --- a/pkg/build/frontend.go +++ b/pkg/build/frontend.go @@ -29,6 +29,7 @@ import ( "github.com/containerd/platforms" dref "github.com/distribution/reference" + "github.com/apple/container-builder-shim/pkg/build/utils" "github.com/moby/buildkit/client/llb" "github.com/moby/buildkit/client/llb/sourceresolver" "github.com/moby/buildkit/exporter/containerimage/exptypes" @@ -42,8 +43,6 @@ import ( "github.com/moby/buildkit/util/progress/progresswriter" ocispecs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" - - "github.com/apple/container-builder-shim/pkg/build/utils" ) 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 stateLock := sync.Mutex{} resolveSource := func(resolvedBaseStageName string, sourcePlatform ocispecs.Platform) error { + + saveState := func(img []byte, fqdn string, ref string, storeName string, opts ...llb.OCILayoutOption) error { + opts = append(opts, llb.Platform(sourcePlatform)) + opts = append(opts, llb.OCIStore("", storeName)) + + st := llb.OCILayout(fqdn, opts...) + + st, err = st.WithImageConfig(img) + if err != nil { + return err + } + + named, err := dref.ParseNormalizedNamed(ref) + if err != nil { + return fmt.Errorf("invalid context name %s %v", ref, err) + } + // pname constructs a platform-qualified image reference in the format buildkit requires for digest resolution + name := strings.TrimSuffix(dref.FamiliarString(named), ":latest") + pname := name + "::" + platforms.FormatAll(platforms.Normalize(sourcePlatform)) + + imgMetaMap := map[string][]byte{ + exptypes.ExporterImageConfigKey: img, + } + imgMeta, err := json.Marshal(imgMetaMap) + if err != nil { + return err + } + + stateLock.Lock() + states[pname] = stateMeta{ + state: st.Platform(sourcePlatform), + imgMeta: imgMeta, + } + stateLock.Unlock() + return nil + } + + // handle build context + if val, ok := bopts.BuildContexts[resolvedBaseStageName]; ok { + // oci-layout requires custom handling as namedContext cannot load from client correctly + if strings.SplitN(val, ":", 2)[0] != "oci-layout" { + return nil + } + // passing in "oci-layout" as they are so client side knows that it is oci from build-context + resolverOpts := sourceresolver.Opt{} + resolverOpts.ImageOpt = &sourceresolver.ResolveImageOpt{ + Platform: &sourcePlatform, + ResolveMode: llb.ResolveModePreferLocal.String(), + } + resolverOpts.OCILayoutOpt = &sourceresolver.ResolveOCILayoutOpt{ + Store: sourceresolver.ResolveImageConfigOptStore{ + StoreID: resolvedBaseStageName, + SessionID: "", + }, + } + _, digest, img, err := bopts.Resolver.ResolveImageConfig(ctx, val, resolverOpts) + if err != nil { + if err == reference.ErrObjectRequired { + return nil + } + return err + } + + // not using the returning `ref` here as the `ref` will be the local path like /User/path/to/oci-layout + // However, when using resolvedBaseStageName directly (ex: deps), + // and if we don't add some dummy host like "docker.io/library/". + // we will get an error like following + // Error: unknown: "failed to solve: failed to load cache key: parse "dummy://deps@sha256:xxx": invalid port ":xxx" after host" + ref := resolvedBaseStageName + fqdn := "docker.io/library/" + ref + "@" + digest.String() + return saveState(img, fqdn, ref, resolvedBaseStageName, llb.WithCustomName("[context "+resolvedBaseStageName+"] OCI load from client")) + } + if strings.EqualFold(resolvedBaseStageName, "scratch") || strings.EqualFold(resolvedBaseStageName, "context") { return nil } @@ -152,7 +224,7 @@ func resolveStates(ctx context.Context, bopts *BOpts, platform ocispecs.Platform } resolverOpts.OCILayoutOpt = &sourceresolver.ResolveOCILayoutOpt{ Store: sourceresolver.ResolveImageConfigOptStore{ - StoreID: "container", + StoreID: KeyContentStoreName, SessionID: "", }, } @@ -175,31 +247,7 @@ func resolveStates(ctx context.Context, bopts *BOpts, platform ocispecs.Platform if _, ok := ref.(dref.Digested); !ok { fqdn += "@" + digest.String() } - st := llb.OCILayout(fqdn, llb.OCIStore("", "container"), llb.Platform(sourcePlatform)) - - named, err := dref.ParseNormalizedNamed(ref.String()) - if err != nil { - return fmt.Errorf("invalid context name %s %v", ref.String(), err) - } - // pname constructs a platform-qualified image reference in the format buildkit requires for digest resolution - name := strings.TrimSuffix(dref.FamiliarString(named), ":latest") - pname := name + "::" + platforms.FormatAll(platforms.Normalize(sourcePlatform)) - - imgMetaMap := map[string][]byte{ - exptypes.ExporterImageConfigKey: img, - } - imgMeta, err := json.Marshal(imgMetaMap) - if err != nil { - return err - } - - stateLock.Lock() - states[pname] = stateMeta{ - state: st.Platform(sourcePlatform), - imgMeta: imgMeta, - } - stateLock.Unlock() - return nil + return saveState(img, fqdn, ref.String(), KeyContentStoreName) } for i, stage := range stages { diff --git a/pkg/fssync/fssync.go b/pkg/fssync/fssync.go index 820f5ce..7db2044 100644 --- a/pkg/fssync/fssync.go +++ b/pkg/fssync/fssync.go @@ -58,10 +58,12 @@ type FSSyncProxy struct { dockerfile []byte dockerignore []byte + + buildContexts map[string]string } func NewFSSyncProxy(contextDir string, basePath string, addedGlobs []string, - dockerfile []byte, dockerignore []byte) (*FSSyncProxy, error) { + dockerfile []byte, dockerignore []byte, buildContexts map[string]string) (*FSSyncProxy, error) { f := new(FSSyncProxy) f.contextDir = contextDir @@ -70,6 +72,7 @@ func NewFSSyncProxy(contextDir string, basePath string, addedGlobs []string, f.dockerfile = dockerfile f.dockerignore = dockerignore + f.buildContexts = buildContexts return f, nil } diff --git a/pkg/fssync/walk.go b/pkg/fssync/walk.go index 52290e6..c9aa3b8 100644 --- a/pkg/fssync/walk.go +++ b/pkg/fssync/walk.go @@ -107,11 +107,17 @@ func (f *FS) Walk(ctx context.Context, target string, fn fs.WalkDirFunc) error { if followPaths == "" { followPaths = strings.Join(f.proxy.addedGlobs, ",") } + root := f.root + if walkMeta.DirName != "" && walkMeta.DirName != "context" && walkMeta.DirName != "scratch" { + if val, ok := f.proxy.buildContexts[walkMeta.DirName]; ok { + root = val + } + } packet := &api.BuildTransfer{ Id: id, Direction: api.TransferDirection_OUTOF, - Source: &f.root, + Source: &root, Metadata: map[string]string{ "os": "linux", "stage": "fssync",