2024-03-06 17:01:00 +08:00
package build
import (
"bufio"
"context"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"syscall"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/content/local"
"github.com/containerd/containerd/platforms"
"github.com/distribution/reference"
"github.com/docker/buildx/builder"
"github.com/docker/buildx/driver"
"github.com/docker/buildx/util/confutil"
"github.com/docker/buildx/util/dockerutil"
"github.com/docker/buildx/util/osutil"
"github.com/docker/buildx/util/progress"
"github.com/docker/docker/builder/remotecontext/urlutil"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/client/ociindex"
gateway "github.com/moby/buildkit/frontend/gateway/client"
"github.com/moby/buildkit/identity"
"github.com/moby/buildkit/session/upload/uploadprovider"
"github.com/moby/buildkit/solver/pb"
"github.com/moby/buildkit/util/apicaps"
"github.com/moby/buildkit/util/entitlements"
"github.com/opencontainers/go-digest"
"github.com/pkg/errors"
2024-03-06 17:05:03 +08:00
"github.com/tonistiigi/fsutil"
2024-03-06 17:01:00 +08:00
)
2024-03-06 17:05:03 +08:00
func toSolveOpt ( ctx context . Context , node builder . Node , multiDriver bool , opt Options , bopts gateway . BuildOpts , configDir string , addVCSLocalDir func ( key , dir string , so * client . SolveOpt ) , pw progress . Writer , docker * dockerutil . Client ) ( _ * client . SolveOpt , release func ( ) , err error ) {
2024-03-06 17:01:00 +08:00
nodeDriver := node . Driver
defers := make ( [ ] func ( ) , 0 , 2 )
releaseF := func ( ) {
for _ , f := range defers {
f ( )
}
}
defer func ( ) {
if err != nil {
releaseF ( )
}
} ( )
// inline cache from build arg
if v , ok := opt . BuildArgs [ "BUILDKIT_INLINE_CACHE" ] ; ok {
if v , _ := strconv . ParseBool ( v ) ; v {
opt . CacheTo = append ( opt . CacheTo , client . CacheOptionsEntry {
Type : "inline" ,
Attrs : map [ string ] string { } ,
} )
}
}
for _ , e := range opt . CacheTo {
if e . Type != "inline" && ! nodeDriver . Features ( ctx ) [ driver . CacheExport ] {
return nil , nil , notSupported ( driver . CacheExport , nodeDriver , "https://docs.docker.com/go/build-cache-backends/" )
}
}
cacheTo := make ( [ ] client . CacheOptionsEntry , 0 , len ( opt . CacheTo ) )
for _ , e := range opt . CacheTo {
if e . Type == "gha" {
if ! bopts . LLBCaps . Contains ( apicaps . CapID ( "cache.gha" ) ) {
continue
}
} else if e . Type == "s3" {
if ! bopts . LLBCaps . Contains ( apicaps . CapID ( "cache.s3" ) ) {
continue
}
}
cacheTo = append ( cacheTo , e )
}
cacheFrom := make ( [ ] client . CacheOptionsEntry , 0 , len ( opt . CacheFrom ) )
for _ , e := range opt . CacheFrom {
if e . Type == "gha" {
if ! bopts . LLBCaps . Contains ( apicaps . CapID ( "cache.gha" ) ) {
continue
}
} else if e . Type == "s3" {
if ! bopts . LLBCaps . Contains ( apicaps . CapID ( "cache.s3" ) ) {
continue
}
}
cacheFrom = append ( cacheFrom , e )
}
so := client . SolveOpt {
Ref : opt . Ref ,
Frontend : "dockerfile.v0" ,
FrontendAttrs : map [ string ] string { } ,
2024-03-06 17:05:03 +08:00
LocalMounts : map [ string ] fsutil . FS { } ,
2024-03-06 17:01:00 +08:00
CacheExports : cacheTo ,
CacheImports : cacheFrom ,
AllowedEntitlements : opt . Allow ,
SourcePolicy : opt . SourcePolicy ,
}
if so . Ref == "" {
so . Ref = identity . NewID ( )
}
if opt . CgroupParent != "" {
so . FrontendAttrs [ "cgroup-parent" ] = opt . CgroupParent
}
if v , ok := opt . BuildArgs [ "BUILDKIT_MULTI_PLATFORM" ] ; ok {
if v , _ := strconv . ParseBool ( v ) ; v {
so . FrontendAttrs [ "multi-platform" ] = "true"
}
}
if multiDriver {
// force creation of manifest list
so . FrontendAttrs [ "multi-platform" ] = "true"
}
attests := make ( map [ string ] string )
for k , v := range opt . Attests {
if v != nil {
attests [ k ] = * v
}
}
supportAttestations := bopts . LLBCaps . Contains ( apicaps . CapID ( "exporter.image.attestations" ) ) && nodeDriver . Features ( ctx ) [ driver . MultiPlatform ]
if len ( attests ) > 0 {
if ! supportAttestations {
if ! nodeDriver . Features ( ctx ) [ driver . MultiPlatform ] {
return nil , nil , notSupported ( "Attestation" , nodeDriver , "https://docs.docker.com/go/attestations/" )
}
return nil , nil , errors . Errorf ( "Attestations are not supported by the current BuildKit daemon" )
}
for k , v := range attests {
so . FrontendAttrs [ "attest:" + k ] = v
}
}
if _ , ok := opt . Attests [ "provenance" ] ; ! ok && supportAttestations {
const noAttestEnv = "BUILDX_NO_DEFAULT_ATTESTATIONS"
var noProv bool
if v , ok := os . LookupEnv ( noAttestEnv ) ; ok {
noProv , err = strconv . ParseBool ( v )
if err != nil {
return nil , nil , errors . Wrap ( err , "invalid " + noAttestEnv )
}
}
if ! noProv {
so . FrontendAttrs [ "attest:provenance" ] = "mode=min,inline-only=true"
}
}
switch len ( opt . Exports ) {
case 1 :
// valid
case 0 :
2024-04-19 09:12:39 +08:00
if ! noDefaultLoad ( ) && opt . PrintFunc == nil {
2024-04-05 21:30:33 +08:00
if nodeDriver . IsMobyDriver ( ) {
// backwards compat for docker driver only:
// this ensures the build results in a docker image.
opt . Exports = [ ] client . ExportEntry { { Type : "image" , Attrs : map [ string ] string { } } }
} else if nodeDriver . Features ( ctx ) [ driver . DefaultLoad ] {
opt . Exports = [ ] client . ExportEntry { { Type : "docker" , Attrs : map [ string ] string { } } }
}
2024-03-06 17:01:00 +08:00
}
default :
if err := bopts . LLBCaps . Supports ( pb . CapMultipleExporters ) ; err != nil {
return nil , nil , errors . Errorf ( "multiple outputs currently unsupported by the current BuildKit daemon, please upgrade to version v0.13+ or use a single output" )
}
}
// fill in image exporter names from tags
if len ( opt . Tags ) > 0 {
tags := make ( [ ] string , len ( opt . Tags ) )
for i , tag := range opt . Tags {
ref , err := reference . Parse ( tag )
if err != nil {
return nil , nil , errors . Wrapf ( err , "invalid tag %q" , tag )
}
tags [ i ] = ref . String ( )
}
for i , e := range opt . Exports {
switch e . Type {
case "image" , "oci" , "docker" :
opt . Exports [ i ] . Attrs [ "name" ] = strings . Join ( tags , "," )
}
}
} else {
for _ , e := range opt . Exports {
if e . Type == "image" && e . Attrs [ "name" ] == "" && e . Attrs [ "push" ] != "" {
if ok , _ := strconv . ParseBool ( e . Attrs [ "push" ] ) ; ok {
return nil , nil , errors . Errorf ( "tag is needed when pushing to registry" )
}
}
}
}
// cacheonly is a fake exporter to opt out of default behaviors
exports := make ( [ ] client . ExportEntry , 0 , len ( opt . Exports ) )
for _ , e := range opt . Exports {
if e . Type != "cacheonly" {
exports = append ( exports , e )
}
}
opt . Exports = exports
// set up exporters
for i , e := range opt . Exports {
if e . Type == "oci" && ! nodeDriver . Features ( ctx ) [ driver . OCIExporter ] {
return nil , nil , notSupported ( driver . OCIExporter , nodeDriver , "https://docs.docker.com/go/build-exporters/" )
}
if e . Type == "docker" {
features := docker . Features ( ctx , e . Attrs [ "context" ] )
if features [ dockerutil . OCIImporter ] && e . Output == nil {
// rely on oci importer if available (which supports
// multi-platform images), otherwise fall back to docker
opt . Exports [ i ] . Type = "oci"
} else if len ( opt . Platforms ) > 1 || len ( attests ) > 0 {
if e . Output != nil {
return nil , nil , errors . Errorf ( "docker exporter does not support exporting manifest lists, use the oci exporter instead" )
}
return nil , nil , errors . Errorf ( "docker exporter does not currently support exporting manifest lists" )
}
if e . Output == nil {
if nodeDriver . IsMobyDriver ( ) {
e . Type = "image"
} else {
w , cancel , err := docker . LoadImage ( ctx , e . Attrs [ "context" ] , pw )
if err != nil {
return nil , nil , err
}
defers = append ( defers , cancel )
opt . Exports [ i ] . Output = func ( _ map [ string ] string ) ( io . WriteCloser , error ) {
return w , nil
}
}
} else if ! nodeDriver . Features ( ctx ) [ driver . DockerExporter ] {
return nil , nil , notSupported ( driver . DockerExporter , nodeDriver , "https://docs.docker.com/go/build-exporters/" )
}
}
if e . Type == "image" && nodeDriver . IsMobyDriver ( ) {
opt . Exports [ i ] . Type = "moby"
if e . Attrs [ "push" ] != "" {
if ok , _ := strconv . ParseBool ( e . Attrs [ "push" ] ) ; ok {
if ok , _ := strconv . ParseBool ( e . Attrs [ "push-by-digest" ] ) ; ok {
return nil , nil , errors . Errorf ( "push-by-digest is currently not implemented for docker driver, please create a new builder instance" )
}
}
}
}
if e . Type == "docker" || e . Type == "image" || e . Type == "oci" {
// inline buildinfo attrs from build arg
if v , ok := opt . BuildArgs [ "BUILDKIT_INLINE_BUILDINFO_ATTRS" ] ; ok {
e . Attrs [ "buildinfo-attrs" ] = v
}
}
}
so . Exports = opt . Exports
so . Session = opt . Session
2024-03-06 17:05:03 +08:00
releaseLoad , err := loadInputs ( ctx , nodeDriver , opt . Inputs , addVCSLocalDir , pw , & so )
2024-03-06 17:01:00 +08:00
if err != nil {
return nil , nil , err
}
defers = append ( defers , releaseLoad )
if sharedKey := so . LocalDirs [ "context" ] ; sharedKey != "" {
if p , err := filepath . Abs ( sharedKey ) ; err == nil {
sharedKey = filepath . Base ( p )
}
so . SharedKey = sharedKey + ":" + confutil . TryNodeIdentifier ( configDir )
}
if opt . Pull {
so . FrontendAttrs [ "image-resolve-mode" ] = pb . AttrImageResolveModeForcePull
} else if nodeDriver . IsMobyDriver ( ) {
// moby driver always resolves local images by default
so . FrontendAttrs [ "image-resolve-mode" ] = pb . AttrImageResolveModePreferLocal
}
if opt . Target != "" {
so . FrontendAttrs [ "target" ] = opt . Target
}
if len ( opt . NoCacheFilter ) > 0 {
so . FrontendAttrs [ "no-cache" ] = strings . Join ( opt . NoCacheFilter , "," )
}
if opt . NoCache {
so . FrontendAttrs [ "no-cache" ] = ""
}
for k , v := range opt . BuildArgs {
so . FrontendAttrs [ "build-arg:" + k ] = v
}
for k , v := range opt . Labels {
so . FrontendAttrs [ "label:" + k ] = v
}
for k , v := range node . ProxyConfig {
if _ , ok := opt . BuildArgs [ k ] ; ! ok {
so . FrontendAttrs [ "build-arg:" + k ] = v
}
}
// set platforms
if len ( opt . Platforms ) != 0 {
pp := make ( [ ] string , len ( opt . Platforms ) )
for i , p := range opt . Platforms {
pp [ i ] = platforms . Format ( p )
}
if len ( pp ) > 1 && ! nodeDriver . Features ( ctx ) [ driver . MultiPlatform ] {
return nil , nil , notSupported ( driver . MultiPlatform , nodeDriver , "https://docs.docker.com/go/build-multi-platform/" )
}
so . FrontendAttrs [ "platform" ] = strings . Join ( pp , "," )
}
// setup networkmode
switch opt . NetworkMode {
case "host" :
so . FrontendAttrs [ "force-network-mode" ] = opt . NetworkMode
so . AllowedEntitlements = append ( so . AllowedEntitlements , entitlements . EntitlementNetworkHost )
case "none" :
so . FrontendAttrs [ "force-network-mode" ] = opt . NetworkMode
case "" , "default" :
default :
return nil , nil , errors . Errorf ( "network mode %q not supported by buildkit - you can define a custom network for your builder using the network driver-opt in buildx create" , opt . NetworkMode )
}
// setup extrahosts
extraHosts , err := toBuildkitExtraHosts ( ctx , opt . ExtraHosts , nodeDriver )
if err != nil {
return nil , nil , err
}
if len ( extraHosts ) > 0 {
so . FrontendAttrs [ "add-hosts" ] = extraHosts
}
// setup shm size
if opt . ShmSize . Value ( ) > 0 {
so . FrontendAttrs [ "shm-size" ] = strconv . FormatInt ( opt . ShmSize . Value ( ) , 10 )
}
// setup ulimits
ulimits , err := toBuildkitUlimits ( opt . Ulimits )
if err != nil {
return nil , nil , err
} else if len ( ulimits ) > 0 {
so . FrontendAttrs [ "ulimit" ] = ulimits
}
2024-04-17 22:49:51 +08:00
// mark info request as internal
if opt . PrintFunc != nil {
so . Internal = true
}
2024-03-06 17:01:00 +08:00
return & so , releaseF , nil
}
2024-03-06 17:05:03 +08:00
func loadInputs ( ctx context . Context , d * driver . DriverHandle , inp Inputs , addVCSLocalDir func ( key , dir string , so * client . SolveOpt ) , pw progress . Writer , target * client . SolveOpt ) ( func ( ) , error ) {
2024-03-06 17:01:00 +08:00
if inp . ContextPath == "" {
return nil , errors . New ( "please specify build context (e.g. \".\" for the current directory)" )
}
// TODO: handle stdin, symlinks, remote contexts, check files exist
var (
err error
dockerfileReader io . Reader
dockerfileDir string
dockerfileName = inp . DockerfilePath
toRemove [ ] string
)
switch {
case inp . ContextState != nil :
if target . FrontendInputs == nil {
target . FrontendInputs = make ( map [ string ] llb . State )
}
target . FrontendInputs [ "context" ] = * inp . ContextState
target . FrontendInputs [ "dockerfile" ] = * inp . ContextState
case inp . ContextPath == "-" :
if inp . DockerfilePath == "-" {
return nil , errStdinConflict
}
buf := bufio . NewReader ( inp . InStream )
magic , err := buf . Peek ( archiveHeaderSize * 2 )
if err != nil && err != io . EOF {
return nil , errors . Wrap ( err , "failed to peek context header from STDIN" )
}
if ! ( err == io . EOF && len ( magic ) == 0 ) {
if isArchive ( magic ) {
// stdin is context
up := uploadprovider . New ( )
target . FrontendAttrs [ "context" ] = up . Add ( buf )
target . Session = append ( target . Session , up )
} else {
if inp . DockerfilePath != "" {
return nil , errDockerfileConflict
}
// stdin is dockerfile
dockerfileReader = buf
inp . ContextPath , _ = os . MkdirTemp ( "" , "empty-dir" )
toRemove = append ( toRemove , inp . ContextPath )
2024-03-06 17:05:03 +08:00
if err := setLocalMount ( "context" , inp . ContextPath , target , addVCSLocalDir ) ; err != nil {
return nil , err
}
2024-03-06 17:01:00 +08:00
}
}
case osutil . IsLocalDir ( inp . ContextPath ) :
2024-03-06 17:05:03 +08:00
if err := setLocalMount ( "context" , inp . ContextPath , target , addVCSLocalDir ) ; err != nil {
return nil , err
}
2024-03-06 17:01:00 +08:00
switch inp . DockerfilePath {
case "-" :
dockerfileReader = inp . InStream
case "" :
dockerfileDir = inp . ContextPath
default :
dockerfileDir = filepath . Dir ( inp . DockerfilePath )
dockerfileName = filepath . Base ( inp . DockerfilePath )
}
case IsRemoteURL ( inp . ContextPath ) :
if inp . DockerfilePath == "-" {
dockerfileReader = inp . InStream
} else if filepath . IsAbs ( inp . DockerfilePath ) {
dockerfileDir = filepath . Dir ( inp . DockerfilePath )
dockerfileName = filepath . Base ( inp . DockerfilePath )
target . FrontendAttrs [ "dockerfilekey" ] = "dockerfile"
}
target . FrontendAttrs [ "context" ] = inp . ContextPath
default :
return nil , errors . Errorf ( "unable to prepare context: path %q not found" , inp . ContextPath )
}
if inp . DockerfileInline != "" {
dockerfileReader = strings . NewReader ( inp . DockerfileInline )
}
if dockerfileReader != nil {
dockerfileDir , err = createTempDockerfile ( dockerfileReader )
if err != nil {
return nil , err
}
toRemove = append ( toRemove , dockerfileDir )
dockerfileName = "Dockerfile"
target . FrontendAttrs [ "dockerfilekey" ] = "dockerfile"
}
if urlutil . IsURL ( inp . DockerfilePath ) {
dockerfileDir , err = createTempDockerfileFromURL ( ctx , d , inp . DockerfilePath , pw )
if err != nil {
return nil , err
}
toRemove = append ( toRemove , dockerfileDir )
dockerfileName = "Dockerfile"
target . FrontendAttrs [ "dockerfilekey" ] = "dockerfile"
delete ( target . FrontendInputs , "dockerfile" )
}
if dockerfileName == "" {
dockerfileName = "Dockerfile"
}
if dockerfileDir != "" {
2024-03-06 17:05:03 +08:00
if err := setLocalMount ( "dockerfile" , dockerfileDir , target , addVCSLocalDir ) ; err != nil {
return nil , err
}
2024-03-06 17:01:00 +08:00
dockerfileName = handleLowercaseDockerfile ( dockerfileDir , dockerfileName )
}
target . FrontendAttrs [ "filename" ] = dockerfileName
for k , v := range inp . NamedContexts {
target . FrontendAttrs [ "frontend.caps" ] = "moby.buildkit.frontend.contexts+forward"
if v . State != nil {
target . FrontendAttrs [ "context:" + k ] = "input:" + k
if target . FrontendInputs == nil {
target . FrontendInputs = make ( map [ string ] llb . State )
}
target . FrontendInputs [ k ] = * v . State
continue
}
if IsRemoteURL ( v . Path ) || strings . HasPrefix ( v . Path , "docker-image://" ) || strings . HasPrefix ( v . Path , "target:" ) {
target . FrontendAttrs [ "context:" + k ] = v . Path
continue
}
// handle OCI layout
if strings . HasPrefix ( v . Path , "oci-layout://" ) {
pathAlone := strings . TrimPrefix ( v . Path , "oci-layout://" )
localPath := pathAlone
localPath , dig , hasDigest := strings . Cut ( localPath , "@" )
localPath , tag , hasTag := strings . Cut ( localPath , ":" )
if ! hasTag {
tag = "latest"
hasTag = true
}
idx := ociindex . NewStoreIndex ( localPath )
if ! hasDigest {
// lookup by name
desc , err := idx . Get ( tag )
if err != nil {
return nil , err
}
if desc != nil {
dig = string ( desc . Digest )
hasDigest = true
}
}
if ! hasDigest {
// lookup single
desc , err := idx . GetSingle ( )
if err != nil {
return nil , err
}
if desc != nil {
dig = string ( desc . Digest )
hasDigest = true
}
}
if ! hasDigest {
return nil , errors . Errorf ( "oci-layout reference %q could not be resolved" , v . Path )
}
_ , err := digest . Parse ( dig )
if err != nil {
return nil , errors . Wrapf ( err , "invalid oci-layout digest %s" , dig )
}
store , err := local . NewStore ( localPath )
if err != nil {
return nil , errors . Wrapf ( err , "invalid store at %s" , localPath )
}
storeName := identity . NewID ( )
if target . OCIStores == nil {
target . OCIStores = map [ string ] content . Store { }
}
target . OCIStores [ storeName ] = store
layout := "oci-layout://" + storeName
if hasTag {
layout += ":" + tag
}
if hasDigest {
layout += "@" + dig
}
target . FrontendAttrs [ "context:" + k ] = layout
continue
}
st , err := os . Stat ( v . Path )
if err != nil {
return nil , errors . Wrapf ( err , "failed to get build context %v" , k )
}
if ! st . IsDir ( ) {
return nil , errors . Wrapf ( syscall . ENOTDIR , "failed to get build context path %v" , v )
}
localName := k
if k == "context" || k == "dockerfile" {
localName = "_" + k // underscore to avoid collisions
}
2024-03-06 17:05:03 +08:00
if err := setLocalMount ( localName , v . Path , target , addVCSLocalDir ) ; err != nil {
return nil , err
}
2024-03-06 17:01:00 +08:00
target . FrontendAttrs [ "context:" + k ] = "local:" + localName
}
release := func ( ) {
for _ , dir := range toRemove {
os . RemoveAll ( dir )
}
}
return release , nil
}
2024-03-06 17:05:03 +08:00
func setLocalMount ( name , root string , so * client . SolveOpt , addVCSLocalDir func ( key , dir string , so * client . SolveOpt ) ) error {
lm , err := fsutil . NewFS ( root )
if err != nil {
return err
}
root , err = filepath . EvalSymlinks ( root ) // keep same behavior as fsutil.NewFS
if err != nil {
return err
}
if so . LocalMounts == nil {
so . LocalMounts = map [ string ] fsutil . FS { }
}
so . LocalMounts [ name ] = lm
if addVCSLocalDir != nil {
addVCSLocalDir ( name , root , so )
}
return nil
}
2024-03-06 17:01:00 +08:00
func createTempDockerfile ( r io . Reader ) ( string , error ) {
dir , err := os . MkdirTemp ( "" , "dockerfile" )
if err != nil {
return "" , err
}
f , err := os . Create ( filepath . Join ( dir , "Dockerfile" ) )
if err != nil {
return "" , err
}
defer f . Close ( )
if _ , err := io . Copy ( f , r ) ; err != nil {
return "" , err
}
return dir , err
}
// handle https://github.com/moby/moby/pull/10858
func handleLowercaseDockerfile ( dir , p string ) string {
if filepath . Base ( p ) != "Dockerfile" {
return p
}
f , err := os . Open ( filepath . Dir ( filepath . Join ( dir , p ) ) )
if err != nil {
return p
}
names , err := f . Readdirnames ( - 1 )
if err != nil {
return p
}
foundLowerCase := false
for _ , n := range names {
if n == "Dockerfile" {
return p
}
if n == "dockerfile" {
foundLowerCase = true
}
}
if foundLowerCase {
return filepath . Join ( filepath . Dir ( p ) , "dockerfile" )
}
return p
}