2022-06-20 12:54:00 -07:00

330 lines
7.5 KiB
Go

package client
import (
"context"
"errors"
"io"
"os"
"path/filepath"
"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"
vconfig "github.com/hashicorp/vagrant-plugin-sdk/config"
"github.com/hashicorp/vagrant-plugin-sdk/helper/path"
"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(),
},
}
c.Cleanup(func() error {
c.logger.Info("stopping local runner",
"runner-id", c.runner.Id())
return c.runner.Close()
})
}
return
}
func (c *Client) LoadBasis(n string) (*Basis, error) {
var basis *vagrant_server.Basis
result, err := c.client.FindBasis(
c.ctx,
&vagrant_server.FindBasisRequest{
Basis: &vagrant_server.Basis{
Name: n,
},
},
)
if err != nil {
if status.Code(err) != codes.NotFound {
return nil, err
}
uresult, err := c.client.UpsertBasis(
c.ctx,
&vagrant_server.UpsertBasisRequest{
Basis: &vagrant_server.Basis{
Name: n,
},
},
)
if err != nil {
return nil, err
}
basis = uresult.Basis
} else {
basis = result.Basis
}
p, err := paths.NamedVagrantConfig(n)
if err != nil {
return nil, err
}
return &Basis{
basis: basis,
client: c,
ctx: c.ctx,
logger: c.logger.Named("basis"),
path: p,
ui: c.ui,
vagrant: c.client,
}, 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
}
}
// Load a Vagrantfile
func LoadVagrantfile(
file path.Path, // path to the Vagrantfile
l hclog.Logger, // logger
c serverclient.RubyVagrantClient, // vagrant ruby runtime for ruby based Vagrantfiles
) (p *vagrant_server.Vagrantfile, err error) {
var v *vconfig.Vagrantfile
p = &vagrant_server.Vagrantfile{}
format := vconfig.JSON
protoFormat := vagrant_server.Vagrantfile_JSON
// We support three types of Vagrantfiles:
// * Ruby (original)
// * HCL
// * JSON (which is HCL in JSON form)
ext := filepath.Ext(file.String())
if ext == ".hcl" {
format = vconfig.HCL
protoFormat = vagrant_server.Vagrantfile_HCL
}
switch ext {
case ".hcl", ".json":
f, err := os.Open(file.String())
if err != nil {
return nil, err
}
p.Raw, err = io.ReadAll(f)
if err != nil {
return nil, err
}
v, err = vconfig.LoadVagrantfile(p.Raw, file.String(), format)
if err != nil {
return nil, err
}
if p.Unfinalized, err = vconfig.EncodeVagrantfile(v); err != nil {
return nil, err
}
default:
p.Unfinalized, err = c.ParseVagrantfile(file.String())
if err != nil {
l.Error("failed to parse vagrantfile",
"error", err,
)
return nil, err
}
l.Info("initial vagrantfile value set",
"path", file.String(),
"value", p.Unfinalized,
)
protoFormat = vagrant_server.Vagrantfile_RUBY
}
p.Path = &vagrant_plugin_sdk.Args_Path{Path: file.String()}
p.Format = protoFormat
return
}