-
Notifications
You must be signed in to change notification settings - Fork 495
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
builder: move builder creation logic to builder package #2150
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,20 +2,28 @@ package builder | |
|
||
import ( | ||
"context" | ||
"encoding/csv" | ||
"encoding/json" | ||
"net/url" | ||
"os" | ||
"sort" | ||
"strings" | ||
"sync" | ||
"time" | ||
|
||
"github.com/docker/buildx/driver" | ||
k8sutil "github.com/docker/buildx/driver/kubernetes/util" | ||
remoteutil "github.com/docker/buildx/driver/remote/util" | ||
"github.com/docker/buildx/localstate" | ||
"github.com/docker/buildx/store" | ||
"github.com/docker/buildx/store/storeutil" | ||
"github.com/docker/buildx/util/confutil" | ||
"github.com/docker/buildx/util/dockerutil" | ||
"github.com/docker/buildx/util/imagetools" | ||
"github.com/docker/buildx/util/progress" | ||
"github.com/docker/cli/cli/command" | ||
dopts "github.com/docker/cli/opts" | ||
"github.com/google/shlex" | ||
"github.com/moby/buildkit/util/progress/progressui" | ||
"github.com/pkg/errors" | ||
"golang.org/x/sync/errgroup" | ||
|
@@ -322,3 +330,315 @@ func GetBuilders(dockerCli command.Cli, txn *store.Txn) ([]*Builder, error) { | |
|
||
return builders, nil | ||
} | ||
|
||
type CreateOpts struct { | ||
Name string | ||
Driver string | ||
NodeName string | ||
Platforms []string | ||
Flags string | ||
ConfigFile string | ||
DriverOpts []string | ||
Use bool | ||
Endpoint string | ||
Append bool | ||
} | ||
|
||
func Create(ctx context.Context, txn *store.Txn, dockerCli command.Cli, opts CreateOpts) (*Builder, error) { | ||
var err error | ||
|
||
if opts.Name == "default" { | ||
return nil, errors.Errorf("default is a reserved name and cannot be used to identify builder instance") | ||
} else if opts.Append && opts.Name == "" { | ||
return nil, errors.Errorf("append requires a builder name") | ||
} | ||
|
||
name := opts.Name | ||
if name == "" { | ||
name, err = store.GenerateName(txn) | ||
if err != nil { | ||
return nil, err | ||
} | ||
} | ||
|
||
if !opts.Append { | ||
contexts, err := dockerCli.ContextStore().List() | ||
if err != nil { | ||
return nil, err | ||
} | ||
for _, c := range contexts { | ||
if c.Name == name { | ||
return nil, errors.Errorf("instance name %q already exists as context builder", name) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as #2150 (comment) |
||
} | ||
} | ||
} | ||
|
||
ng, err := txn.NodeGroupByName(name) | ||
if err != nil { | ||
if os.IsNotExist(errors.Cause(err)) { | ||
if opts.Append && opts.Name != "" { | ||
return nil, errors.Errorf("failed to find instance %q for append", opts.Name) | ||
} | ||
} else { | ||
return nil, err | ||
} | ||
} | ||
|
||
buildkitHost := os.Getenv("BUILDKIT_HOST") | ||
|
||
driverName := opts.Driver | ||
if driverName == "" { | ||
if ng != nil { | ||
driverName = ng.Driver | ||
} else if opts.Endpoint == "" && buildkitHost != "" { | ||
driverName = "remote" | ||
} else { | ||
f, err := driver.GetDefaultFactory(ctx, opts.Endpoint, dockerCli.Client(), true, nil) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if f == nil { | ||
return nil, errors.Errorf("no valid drivers found") | ||
} | ||
driverName = f.Name() | ||
} | ||
} | ||
|
||
if ng != nil { | ||
if opts.NodeName == "" && !opts.Append { | ||
return nil, errors.Errorf("existing instance for %q but no append mode, specify the node name to make changes for existing instances", name) | ||
} | ||
if driverName != ng.Driver { | ||
return nil, errors.Errorf("existing instance for %q but has mismatched driver %q", name, ng.Driver) | ||
} | ||
} | ||
|
||
if _, err := driver.GetFactory(driverName, true); err != nil { | ||
return nil, err | ||
} | ||
|
||
ngOriginal := ng | ||
if ngOriginal != nil { | ||
ngOriginal = ngOriginal.Copy() | ||
} | ||
|
||
if ng == nil { | ||
ng = &store.NodeGroup{ | ||
Name: name, | ||
Driver: driverName, | ||
} | ||
} | ||
|
||
var flags []string | ||
if opts.Flags != "" { | ||
flags, err = shlex.Split(opts.Flags) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "failed to parse buildkit flags") | ||
} | ||
} | ||
|
||
var ep string | ||
var setEp bool | ||
switch { | ||
case driverName == "kubernetes": | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ideally, these driver specifics can be removed from this package eventually, but not new code as this is how it was in |
||
if opts.Endpoint != "" { | ||
return nil, errors.Errorf("kubernetes driver does not support endpoint args %q", opts.Endpoint) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as #2150 (comment) |
||
} | ||
// generate node name if not provided to avoid duplicated endpoint | ||
// error: https://github.com/docker/setup-buildx-action/issues/215 | ||
nodeName := opts.NodeName | ||
if nodeName == "" { | ||
nodeName, err = k8sutil.GenerateNodeName(name, txn) | ||
if err != nil { | ||
return nil, err | ||
} | ||
} | ||
// naming endpoint to make append works | ||
ep = (&url.URL{ | ||
Scheme: driverName, | ||
Path: "/" + name, | ||
RawQuery: (&url.Values{ | ||
"deployment": {nodeName}, | ||
"kubeconfig": {os.Getenv("KUBECONFIG")}, | ||
}).Encode(), | ||
}).String() | ||
setEp = false | ||
case driverName == "remote": | ||
if opts.Endpoint != "" { | ||
ep = opts.Endpoint | ||
} else if buildkitHost != "" { | ||
ep = buildkitHost | ||
} else { | ||
return nil, errors.Errorf("no remote endpoint provided") | ||
} | ||
ep, err = validateBuildkitEndpoint(ep) | ||
if err != nil { | ||
return nil, err | ||
} | ||
setEp = true | ||
case opts.Endpoint != "": | ||
ep, err = validateEndpoint(dockerCli, opts.Endpoint) | ||
if err != nil { | ||
return nil, err | ||
} | ||
setEp = true | ||
default: | ||
if dockerCli.CurrentContext() == "default" && dockerCli.DockerEndpoint().TLSData != nil { | ||
return nil, 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 context set to <context-name>") | ||
} | ||
ep, err = dockerutil.GetCurrentEndpoint(dockerCli) | ||
if err != nil { | ||
return nil, err | ||
} | ||
setEp = false | ||
} | ||
|
||
m, err := csvToMap(opts.DriverOpts) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
configFile := opts.ConfigFile | ||
if configFile == "" { | ||
// if buildkit config is not provided, check if the default one is | ||
// available and use it | ||
if f, ok := confutil.DefaultConfigFile(dockerCli); ok { | ||
configFile = f | ||
} | ||
} | ||
|
||
if err := ng.Update(opts.NodeName, ep, opts.Platforms, setEp, opts.Append, flags, configFile, m); err != nil { | ||
return nil, err | ||
} | ||
|
||
if err := txn.Save(ng); err != nil { | ||
return nil, err | ||
} | ||
|
||
b, err := New(dockerCli, | ||
WithName(ng.Name), | ||
WithStore(txn), | ||
WithSkippedValidation(), | ||
) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
timeoutCtx, cancel := context.WithTimeout(ctx, 20*time.Second) | ||
defer cancel() | ||
|
||
nodes, err := b.LoadNodes(timeoutCtx, WithData()) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
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) | ||
var err2 error | ||
if ngOriginal == nil { | ||
err2 = txn.Remove(ng.Name) | ||
} else { | ||
err2 = txn.Save(ngOriginal) | ||
} | ||
if err2 != nil { | ||
return nil, errors.Errorf("could not rollback to previous state: %s", err2) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as #2150 (comment) |
||
} | ||
return nil, err | ||
} | ||
} | ||
|
||
if opts.Use && ep != "" { | ||
current, err := dockerutil.GetCurrentEndpoint(dockerCli) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if err := txn.SetCurrent(current, ng.Name, false, false); err != nil { | ||
return nil, err | ||
} | ||
} | ||
|
||
return b, nil | ||
} | ||
|
||
type LeaveOpts struct { | ||
Name string | ||
NodeName string | ||
} | ||
|
||
func Leave(ctx context.Context, txn *store.Txn, dockerCli command.Cli, opts LeaveOpts) error { | ||
if opts.Name == "" { | ||
return errors.Errorf("leave requires instance name") | ||
} | ||
if opts.NodeName == "" { | ||
return errors.Errorf("leave requires node name") | ||
} | ||
|
||
ng, err := txn.NodeGroupByName(opts.Name) | ||
if err != nil { | ||
if os.IsNotExist(errors.Cause(err)) { | ||
return errors.Errorf("failed to find instance %q for leave", opts.Name) | ||
} | ||
return err | ||
} | ||
|
||
if err := ng.Leave(opts.NodeName); err != nil { | ||
return err | ||
} | ||
|
||
ls, err := localstate.New(confutil.ConfigDir(dockerCli)) | ||
if err != nil { | ||
return err | ||
} | ||
if err := ls.RemoveBuilderNode(ng.Name, opts.NodeName); err != nil { | ||
return err | ||
} | ||
|
||
return txn.Save(ng) | ||
} | ||
|
||
func csvToMap(in []string) (map[string]string, error) { | ||
if len(in) == 0 { | ||
return nil, nil | ||
} | ||
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 | ||
} | ||
|
||
// 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 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
package commands | ||
package builder | ||
|
||
import ( | ||
"testing" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now returns an error instead of a warning log:
buildx/commands/create.go
Line 67 in b2f705a
I think being more strict about it is fine but let me know if we should typed this kind error and handle it it differently in
command
package when invoked through the CLI to still log a warning or just pass a logger toCreate
.