2019-04-12 08:24:24 +08:00
package commands
import (
2021-09-29 21:33:19 +08:00
"bytes"
2021-08-12 14:45:40 +08:00
"context"
2019-08-01 08:25:25 +08:00
"encoding/csv"
2019-04-12 08:24:24 +08:00
"fmt"
2020-12-30 13:59:51 +08:00
"net/url"
2019-04-13 07:39:06 +08:00
"os"
2019-08-01 08:25:25 +08:00
"strings"
2021-08-12 14:45:40 +08:00
"time"
2019-04-12 08:24:24 +08:00
2022-12-06 02:57:35 +08:00
"github.com/docker/buildx/builder"
2019-04-25 10:29:56 +08:00
"github.com/docker/buildx/driver"
2023-03-12 03:10:46 +08:00
k8sutil "github.com/docker/buildx/driver/kubernetes/util"
2022-12-06 02:57:35 +08:00
remoteutil "github.com/docker/buildx/driver/remote/util"
2019-04-25 10:29:56 +08:00
"github.com/docker/buildx/store"
2021-11-04 12:17:27 +08:00
"github.com/docker/buildx/store/storeutil"
2021-11-05 01:52:11 +08:00
"github.com/docker/buildx/util/cobrautil"
2022-05-13 17:53:53 +08:00
"github.com/docker/buildx/util/confutil"
2022-11-28 22:47:40 +08:00
"github.com/docker/buildx/util/dockerutil"
2019-04-12 08:24:24 +08:00
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
2022-12-06 02:57:35 +08:00
dopts "github.com/docker/cli/opts"
2019-07-09 06:06:07 +08:00
"github.com/google/shlex"
2019-04-13 07:39:06 +08:00
"github.com/moby/buildkit/util/appcontext"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
2019-04-12 08:24:24 +08:00
"github.com/spf13/cobra"
)
type createOptions struct {
name string
driver string
nodeName string
platform [ ] string
actionAppend bool
actionLeave bool
2019-04-13 07:39:06 +08:00
use bool
2019-07-09 06:06:07 +08:00
flags string
2019-08-01 01:54:20 +08:00
configFile string
2019-08-01 08:25:25 +08:00
driverOpts [ ] string
2021-08-12 14:45:40 +08:00
bootstrap bool
2019-04-12 08:24:24 +08:00
// upgrade bool // perform upgrade of the driver
}
2019-04-13 07:39:06 +08:00
func runCreate ( dockerCli command . Cli , in createOptions , args [ ] string ) error {
ctx := appcontext . Context ( )
if in . name == "default" {
2019-04-16 08:01:00 +08:00
return errors . Errorf ( "default is a reserved name and cannot be used to identify builder instance" )
2019-04-13 07:39:06 +08:00
}
if in . actionLeave {
if in . name == "" {
return errors . Errorf ( "leave requires instance name" )
}
if in . nodeName == "" {
return errors . Errorf ( "leave requires node name but --node not set" )
}
}
if in . actionAppend {
if in . name == "" {
logrus . Warnf ( "append used without name, creating a new instance instead" )
}
}
2021-11-04 12:17:27 +08:00
txn , release , err := storeutil . GetStore ( dockerCli )
2019-04-13 07:39:06 +08:00
if err != nil {
return err
}
defer release ( )
name := in . name
if name == "" {
name , err = store . GenerateName ( txn )
if err != nil {
return err
}
}
2022-05-19 23:17:28 +08:00
if ! in . actionLeave && ! in . actionAppend {
contexts , err := dockerCli . ContextStore ( ) . List ( )
if err != nil {
return err
}
for _ , c := range contexts {
if c . Name == name {
logrus . Warnf ( "instance name %q already exists as context builder" , name )
break
}
}
}
2019-04-13 07:39:06 +08:00
ng , err := txn . NodeGroupByName ( name )
if err != nil {
if os . IsNotExist ( errors . Cause ( err ) ) {
if in . actionAppend && in . name != "" {
logrus . Warnf ( "failed to find %q for append, creating a new instance instead" , in . name )
}
if in . actionLeave {
2022-06-28 17:16:18 +08:00
return errors . Errorf ( "failed to find instance %q for leave" , in . name )
2019-04-13 07:39:06 +08:00
}
} else {
return err
}
}
2022-06-28 17:16:18 +08:00
buildkitHost := os . Getenv ( "BUILDKIT_HOST" )
driverName := in . driver
if driverName == "" {
if ng != nil {
driverName = ng . Driver
} else if len ( args ) == 0 && buildkitHost != "" {
driverName = "remote"
} else {
var arg string
if len ( args ) > 0 {
arg = args [ 0 ]
}
f , err := driver . GetDefaultFactory ( ctx , arg , dockerCli . Client ( ) , true )
if err != nil {
return err
}
if f == nil {
return errors . Errorf ( "no valid drivers found" )
}
driverName = f . Name ( )
}
}
2019-04-16 08:01:00 +08:00
if ng != nil {
if in . nodeName == "" && ! in . actionAppend {
2022-06-28 17:16:18 +08:00
return errors . Errorf ( "existing instance for %q but no append mode, specify --node to make changes for existing instances" , name )
2019-04-16 08:01:00 +08:00
}
2022-06-28 17:16:18 +08:00
if driverName != ng . Driver {
return errors . Errorf ( "existing instance for %q but has mismatched driver %q" , name , ng . Driver )
}
}
2022-08-16 17:58:23 +08:00
if _ , err := driver . GetFactory ( driverName , true ) ; err != nil {
return err
2019-04-16 08:01:00 +08:00
}
2022-07-27 18:48:52 +08:00
ngOriginal := ng
if ngOriginal != nil {
ngOriginal = ngOriginal . Copy ( )
}
2019-04-13 07:39:06 +08:00
if ng == nil {
ng = & store . NodeGroup {
2022-06-28 17:16:18 +08:00
Name : name ,
Driver : driverName ,
2019-04-13 07:39:06 +08:00
}
}
2019-07-09 06:06:07 +08:00
var flags [ ] string
if in . flags != "" {
flags , err = shlex . Split ( in . flags )
if err != nil {
return errors . Wrap ( err , "failed to parse buildkit flags" )
}
}
2019-04-13 07:39:06 +08:00
var ep string
2022-04-25 18:28:40 +08:00
var setEp bool
2019-04-13 07:39:06 +08:00
if in . actionLeave {
if err := ng . Leave ( in . nodeName ) ; err != nil {
return err
}
} else {
2022-04-25 18:28:40 +08:00
switch {
case driverName == "kubernetes" :
2022-06-28 17:03:50 +08:00
if len ( args ) > 0 {
logrus . Warnf ( "kubernetes driver does not support endpoint args %q" , args [ 0 ] )
}
2023-03-12 03:10:46 +08:00
// generate node name if not provided to avoid duplicated endpoint
// error: https://github.com/docker/setup-buildx-action/issues/215
nodeName := in . nodeName
if nodeName == "" {
nodeName , err = k8sutil . GenerateNodeName ( name , txn )
if err != nil {
return err
}
}
2022-04-25 18:28:40 +08:00
// naming endpoint to make --append works
ep = ( & url . URL {
Scheme : driverName ,
2023-03-12 03:10:46 +08:00
Path : "/" + name ,
2022-04-25 18:28:40 +08:00
RawQuery : ( & url . Values {
2023-03-12 03:10:46 +08:00
"deployment" : { nodeName } ,
2022-04-25 18:28:40 +08:00
"kubeconfig" : { os . Getenv ( "KUBECONFIG" ) } ,
} ) . Encode ( ) ,
} ) . String ( )
setEp = false
case driverName == "remote" :
if len ( args ) > 0 {
ep = args [ 0 ]
} else if buildkitHost != "" {
ep = buildkitHost
} else {
return errors . Errorf ( "no remote endpoint provided" )
}
ep , err = validateBuildkitEndpoint ( ep )
if err != nil {
return err
}
setEp = true
case len ( args ) > 0 :
2019-04-13 07:39:06 +08:00
ep , err = validateEndpoint ( dockerCli , args [ 0 ] )
if err != nil {
return err
}
2022-04-25 18:28:40 +08:00
setEp = true
default :
2019-04-23 05:26:18 +08:00
if dockerCli . CurrentContext ( ) == "default" && dockerCli . DockerEndpoint ( ) . TLSData != nil {
return errors . Errorf ( "could not create a builder instance with TLS data loaded from environment. Please use `docker context create <context-name>` to create a context for current environment and then create a builder instance with `docker buildx create <context-name>`" )
}
2022-11-28 22:47:40 +08:00
ep , err = dockerutil . GetCurrentEndpoint ( dockerCli )
2019-04-13 07:39:06 +08:00
if err != nil {
return err
}
2022-04-25 18:28:40 +08:00
setEp = false
2020-08-28 14:51:58 +08:00
}
2019-08-01 08:25:25 +08:00
m , err := csvToMap ( in . driverOpts )
if err != nil {
return err
}
2022-05-13 17:53:53 +08:00
if in . configFile == "" {
// if buildkit config is not provided, check if the default one is
// available and use it
if f , ok := confutil . DefaultConfigFile ( dockerCli ) ; ok {
logrus . Warnf ( "Using default BuildKit config in %s" , f )
in . configFile = f
}
}
2022-04-25 18:28:40 +08:00
if err := ng . Update ( in . nodeName , ep , in . platform , setEp , in . actionAppend , flags , in . configFile , m ) ; err != nil {
2019-04-13 07:39:06 +08:00
return err
}
}
if err := txn . Save ( ng ) ; err != nil {
return err
}
2022-12-06 02:57:35 +08:00
b , err := builder . New ( dockerCli ,
builder . WithName ( ng . Name ) ,
builder . WithStore ( txn ) ,
builder . WithSkippedValidation ( ) ,
)
if err != nil {
return err
}
2021-08-12 14:45:40 +08:00
timeoutCtx , cancel := context . WithTimeout ( ctx , 20 * time . Second )
defer cancel ( )
2022-12-06 02:57:35 +08:00
nodes , err := b . LoadNodes ( timeoutCtx , true )
if err != nil {
2021-08-12 14:45:40 +08:00
return err
}
2022-12-06 02:57:35 +08:00
for _ , node := range nodes {
if err := node . Err ; err != nil {
err := errors . Errorf ( "failed to initialize builder %s (%s): %s" , ng . Name , node . Name , err )
2022-07-27 18:48:52 +08:00
var err2 error
if ngOriginal == nil {
err2 = txn . Remove ( ng . Name )
} else {
err2 = txn . Save ( ngOriginal )
}
if err2 != nil {
logrus . Warnf ( "Could not rollback to previous state: %s" , err2 )
}
return err
}
}
if in . use && ep != "" {
2022-11-28 22:47:40 +08:00
current , err := dockerutil . GetCurrentEndpoint ( dockerCli )
2022-07-27 18:48:52 +08:00
if err != nil {
return err
}
if err := txn . SetCurrent ( current , ng . Name , false , false ) ; err != nil {
return err
2022-07-12 20:10:05 +08:00
}
}
2021-08-12 14:45:40 +08:00
if in . bootstrap {
2022-12-06 02:57:35 +08:00
if _ , err = b . Boot ( ctx ) ; err != nil {
2021-08-12 14:45:40 +08:00
return err
}
}
2019-04-13 07:39:06 +08:00
fmt . Printf ( "%s\n" , ng . Name )
2019-04-12 08:24:24 +08:00
return nil
}
func createCmd ( dockerCli command . Cli ) * cobra . Command {
var options createOptions
2021-09-29 21:33:19 +08:00
var drivers bytes . Buffer
2022-08-16 17:58:23 +08:00
for _ , d := range driver . GetFactories ( true ) {
2021-09-29 21:33:19 +08:00
if len ( drivers . String ( ) ) > 0 {
drivers . WriteString ( ", " )
}
2021-11-22 17:51:54 +08:00
drivers . WriteString ( fmt . Sprintf ( ` "%s" ` , d . Name ( ) ) )
2019-05-26 06:45:20 +08:00
}
2019-04-12 08:24:24 +08:00
cmd := & cobra . Command {
Use : "create [OPTIONS] [CONTEXT|ENDPOINT]" ,
Short : "Create a new builder instance" ,
Args : cli . RequiresMaxArgs ( 1 ) ,
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
2019-04-13 07:39:06 +08:00
return runCreate ( dockerCli , options , args )
2019-04-12 08:24:24 +08:00
} ,
}
flags := cmd . Flags ( )
flags . StringVar ( & options . name , "name" , "" , "Builder instance name" )
2021-09-29 21:33:19 +08:00
flags . StringVar ( & options . driver , "driver" , "" , fmt . Sprintf ( "Driver to use (available: %s)" , drivers . String ( ) ) )
2019-04-12 08:24:24 +08:00
flags . StringVar ( & options . nodeName , "node" , "" , "Create/modify node with given name" )
2019-07-09 06:06:07 +08:00
flags . StringVar ( & options . flags , "buildkitd-flags" , "" , "Flags for buildkitd daemon" )
2019-08-01 01:54:20 +08:00
flags . StringVar ( & options . configFile , "config" , "" , "BuildKit config file" )
2019-04-12 08:24:24 +08:00
flags . StringArrayVar ( & options . platform , "platform" , [ ] string { } , "Fixed platforms for current node" )
2019-08-01 08:25:25 +08:00
flags . StringArrayVar ( & options . driverOpts , "driver-opt" , [ ] string { } , "Options for the driver" )
2021-08-12 14:45:40 +08:00
flags . BoolVar ( & options . bootstrap , "bootstrap" , false , "Boot builder after creation" )
2019-04-12 08:24:24 +08:00
flags . BoolVar ( & options . actionAppend , "append" , false , "Append a node to builder instead of changing it" )
flags . BoolVar ( & options . actionLeave , "leave" , false , "Remove a node from builder instead of changing it" )
2019-04-13 07:39:06 +08:00
flags . BoolVar ( & options . use , "use" , false , "Set the current builder instance" )
2019-04-12 08:24:24 +08:00
2021-11-05 01:52:11 +08:00
// hide builder persistent flag for this command
cobrautil . HideInheritedFlags ( cmd , "builder" )
2019-04-12 08:24:24 +08:00
return cmd
}
2019-08-01 08:25:25 +08:00
func csvToMap ( in [ ] string ) ( map [ string ] string , error ) {
2022-07-27 19:06:47 +08:00
if len ( in ) == 0 {
return nil , nil
}
2019-08-01 08:25:25 +08:00
m := make ( map [ string ] string , len ( in ) )
for _ , s := range in {
csvReader := csv . NewReader ( strings . NewReader ( s ) )
fields , err := csvReader . Read ( )
if err != nil {
return nil , err
}
for _ , v := range fields {
p := strings . SplitN ( v , "=" , 2 )
if len ( p ) != 2 {
return nil , errors . Errorf ( "invalid value %q, expecting k=v" , v )
}
m [ p [ 0 ] ] = p [ 1 ]
}
}
return m , nil
}
2022-12-06 02:57:35 +08:00
// validateEndpoint validates that endpoint is either a context or a docker host
func validateEndpoint ( dockerCli command . Cli , ep string ) ( string , error ) {
dem , err := dockerutil . GetDockerEndpoint ( dockerCli , ep )
if err == nil && dem != nil {
if ep == "default" {
return dem . Host , nil
}
return ep , nil
}
h , err := dopts . ParseHost ( true , ep )
if err != nil {
return "" , errors . Wrapf ( err , "failed to parse endpoint %s" , ep )
}
return h , nil
}
// validateBuildkitEndpoint validates that endpoint is a valid buildkit host
func validateBuildkitEndpoint ( ep string ) ( string , error ) {
if err := remoteutil . IsValidEndpoint ( ep ) ; err != nil {
return "" , err
}
return ep , nil
}