// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package client import ( "context" "errors" "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-multierror" "github.com/hashicorp/go-plugin" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" sdkconfig "github.com/hashicorp/vagrant-plugin-sdk/config" "github.com/hashicorp/vagrant-plugin-sdk/helper/paths" "github.com/hashicorp/vagrant-plugin-sdk/internal-shared/cleanup" "github.com/hashicorp/vagrant-plugin-sdk/proto/vagrant_plugin_sdk" "github.com/hashicorp/vagrant-plugin-sdk/terminal" "github.com/hashicorp/vagrant/internal/config" "github.com/hashicorp/vagrant/internal/runner" "github.com/hashicorp/vagrant/internal/server/proto/vagrant_server" "github.com/hashicorp/vagrant/internal/serverclient" ) var ( NotFoundErr = errors.New("failed to locate requested resource") ) type Client struct { config *config.Config cleanup cleanup.Cleanup client *serverclient.VagrantClient ctx context.Context localRunner bool localServer bool logger hclog.Logger rubyRuntime plugin.ClientProtocol runner *runner.Runner runnerRef *vagrant_server.Ref_Runner ui terminal.UI } func New(ctx context.Context, opts ...Option) (c *Client, err error) { c = &Client{ cleanup: cleanup.New(), ctx: ctx, logger: hclog.L().Named("vagrant.client"), runnerRef: &vagrant_server.Ref_Runner{ Target: &vagrant_server.Ref_Runner_Any{ Any: &vagrant_server.Ref_RunnerAny{}, }, }, } // If an error was encountered, ensure that // we return back a nil value for the client defer func() { if err != nil { c = nil } }() // Apply any provided options var cfg clientConfig for _, opt := range opts { if e := opt(c, &cfg); e != nil { err = multierror.Append(err, e) } } if err != nil { return } // If no UI is configured, create a default if c.ui == nil { c.ui = terminal.ConsoleUI(ctx) } // If no client is configured, establish a new connection // or spin up an in-process server if c.client == nil { conn, err := c.initServerClient(context.Background(), &cfg) if err != nil { c.logger.Error("failed to establish server connection", "error", err) return nil, err } c.client = serverclient.WrapVagrantClient(conn) c.logger.Info("established connection to vagrant server") } else { c.logger.Warn("using provided client for vagrant server connection") } // Negotiate the version if err = c.negotiateApiVersion(ctx); err != nil { return } // If no Ruby runtime is configured, start one if c.rubyRuntime == nil { if c.rubyRuntime, err = c.initVagrantRubyRuntime(); err != nil { c.logger.Error("failed to start vagrant ruby runtime", "error", err) return } } // If we are using a local runner, spin it up if c.localRunner { c.runner, err = c.startRunner() if err != nil { return } c.logger.Info("started local runner", "runner-id", c.runner.Id()) // Set our local runner as the target c.runnerRef.Target = &vagrant_server.Ref_Runner_Id{ Id: &vagrant_server.Ref_RunnerId{ Id: c.runner.Id(), }, } // Prepend our runner cleanup so that it // can properly shutdown everything before // the server is halted if we are running // a local server c.cleanup.Prepend(func() error { c.logger.Info("stopping local runner", "runner-id", c.runner.Id()) return c.runner.Close() }) } return } func (c *Client) LoadBasis(n string) (*vagrant_server.Basis, error) { var basis *vagrant_server.Basis p, err := paths.NamedVagrantConfig(n) if err != nil { return nil, err } basisVagrantfile, err := sdkconfig.FindPath(p, nil) if err != nil { return nil, err } result, err := c.client.FindBasis( c.ctx, &vagrant_server.FindBasisRequest{ Basis: &vagrant_server.Basis{ Name: n, Path: p.String(), }, }, ) if err != nil && status.Code(err) != codes.NotFound { return nil, err } if err == nil { basis = result.Basis } else { basis = &vagrant_server.Basis{ Name: n, Path: p.String(), } } if err == nil { basis = result.Basis } else { basis = &vagrant_server.Basis{ Name: n, Path: p.String(), } } basis.Configuration = &vagrant_server.Vagrantfile{ Path: &vagrant_plugin_sdk.Args_Path{ Path: basisVagrantfile.String(), }, } uresult, err := c.client.UpsertBasis( c.ctx, &vagrant_server.UpsertBasisRequest{ Basis: basis, }, ) if err != nil { return nil, err } basis = uresult.Basis return basis, nil } func (c *Client) LoadProject(n string, b *vagrant_plugin_sdk.Ref_Basis) (*vagrant_server.Project, error) { var project *vagrant_server.Project projectVagrantfile, err := sdkconfig.FindPath(nil, nil) if err != nil { return nil, err } // No project was found, so return if projectVagrantfile == nil { return nil, nil } projectDir := projectVagrantfile.Dir() if n == "" { n = projectDir.String() } result, err := c.client.FindProject( c.ctx, &vagrant_server.FindProjectRequest{ Project: &vagrant_server.Project{ Name: n, Path: projectDir.String(), Basis: b, }, }, ) if err != nil && status.Code(err) != codes.NotFound { return nil, err } if err == nil { project = result.Project } else { project = &vagrant_server.Project{ Name: n, Path: projectDir.String(), Basis: b, } } project.Configuration = &vagrant_server.Vagrantfile{ Path: &vagrant_plugin_sdk.Args_Path{ Path: projectVagrantfile.String(), }, } uresult, err := c.client.UpsertProject( c.ctx, &vagrant_server.UpsertProjectRequest{ Project: project, }, ) if err != nil { return nil, err } project = uresult.Project return project, nil } // Close the client and call any cleanup functions // that have been defined func (c *Client) Close() (err error) { return c.cleanup.Close() } func (c *Client) Cleanup(fn cleanup.CleanupFn) { c.cleanup.Do(fn) } func (c *Client) UI() terminal.UI { return c.ui } type clientConfig struct { connectOpts []serverclient.ConnectOption } type Option func(*Client, *clientConfig) error // WithClient sets the client directly. In this case, the runner won't // attempt any connection at all regardless of other configuration (env // vars or vagrant config file). This will be used. func WithClient(client *serverclient.VagrantClient) Option { return func(c *Client, cfg *clientConfig) error { if client != nil { c.client = client } return nil } } // WithClientConnect specifies the options for connecting to a client. // If WithClient is specified, that client is always used. // // If WithLocal is set and no client is specified and no server creds // can be found, then an in-process server will be created. func WithClientConnect(opts ...serverclient.ConnectOption) Option { return func(_ *Client, cfg *clientConfig) error { cfg.connectOpts = opts return nil } } // WithLocal puts the client in local exec mode. In this mode, the client // will spin up a per-operation runner locally and reference the local on-disk // data for all operations. func WithLocal() Option { return func(c *Client, cfg *clientConfig) error { c.localRunner = true return nil } } // WithLogger sets the logger for the client. func WithLogger(log hclog.Logger) Option { return func(c *Client, cfg *clientConfig) error { c.logger = log return nil } } // WithUI sets the UI to use for the client. func WithUI(ui terminal.UI) Option { return func(c *Client, cfg *clientConfig) error { c.ui = ui return nil } } // Register cleanup callback func WithCleanup(f func() error) Option { return func(c *Client, cfg *clientConfig) error { c.Cleanup(f) return nil } } func WithConfig(cfg *config.Config) Option { return func(c *Client, _ *clientConfig) error { c.config = cfg return nil } }