2022-04-25 12:23:57 -05:00

234 lines
6.6 KiB
Go

package client
import (
"context"
"io"
"net"
"os/exec"
"path/filepath"
"runtime"
"time"
"github.com/boltdb/bolt"
"github.com/golang/protobuf/ptypes/empty"
"github.com/hashicorp/go-plugin"
"google.golang.org/grpc"
"github.com/hashicorp/vagrant-plugin-sdk/proto/vagrant_plugin_sdk"
"github.com/hashicorp/vagrant/internal/protocolversion"
"github.com/hashicorp/vagrant/internal/server"
sr "github.com/hashicorp/vagrant/internal/server"
"github.com/hashicorp/vagrant/internal/server/proto/vagrant_server"
"github.com/hashicorp/vagrant/internal/server/singleprocess"
"github.com/hashicorp/vagrant/internal/serverclient"
)
// initServerClient will initialize a gRPC connection to the Vagrant server.
// This is called if a client wasn't explicitly given with WithClient.
//
// If a connection is successfully established, this will register connection
// closing and server cleanup with the Project cleanup function.
//
// This function will do one of two things:
//
// 1. If connection options were given, it'll attempt to connect to
// an existing Vagrant server.
//
// 2. If WithLocal was specified and no connection addresses can be
// found, this will spin up an in-memory server.
//
func (b *Basis) initServerClient(ctx context.Context, cfg *config) (*grpc.ClientConn, error) {
log := b.logger.Named("server")
// If we're local, then connection is optional.
opts := cfg.connectOpts
if b.local {
log.Trace("WithLocal set, server credentials optional")
opts = append(opts, serverclient.Optional())
}
// Connect. If we're local, this is set as optional so conn may be nil
log.Info("attempting to source credentials and connect")
conn, err := serverclient.Connect(ctx, opts...)
if err != nil {
return nil, err
}
// If we established a connection
if conn != nil {
log.Debug("connection established with sourced credentials")
b.cleanup(func() { conn.Close() })
return conn, nil
}
// No connection, meaning we have to spin up a local server. This
// can only be reached if we specified "Optional" to serverclient
// which is only possible if we configured this client to support local
// mode.
log.Info("no server credentials found, using in-memory local server")
return b.initLocalServer(ctx)
}
// initLocalServer starts the local server and configures p.client to
// point to it. This also configures p.localClosers so that all the
// resources are properly cleaned up on Close.
//
// If this returns an error, all resources associated with this operation
// will be closed, but the project can retry.
func (b *Basis) initLocalServer(ctx context.Context) (*grpc.ClientConn, error) {
log := b.logger.Named("server")
b.localServer = true
// We use this pointer to accumulate things we need to clean up
// in the case of an error. On success we nil this variable which
// doesn't close anything.
var closers []io.Closer
defer func() {
for _, c := range closers {
c.Close()
}
}()
// TODO(mitchellh): path to this
path := filepath.Join("data.db")
log.Debug("opening local mode DB", "path", path)
// Open our database
db, err := bolt.Open(path, 0600, &bolt.Options{
Timeout: 1 * time.Second,
})
if err != nil {
return nil, err
}
closers = append(closers, db)
vrr, err := b.initVagrantRubyRuntime()
if err != nil {
return nil, err
}
// Create our server
impl, err := singleprocess.New(
singleprocess.WithVagrantRubyRuntime(vrr),
singleprocess.WithDB(db),
singleprocess.WithLogger(log.Named("singleprocess")),
)
if err != nil {
log.Trace("failed singleprocess server setup", "error", err)
return nil, err
}
// We listen on a random locally bound port
// TODO: we should use Unix domain sockets if supported
ln, err := net.Listen("tcp", "127.0.0.1:")
if err != nil {
return nil, err
}
closers = append(closers, ln)
// Create a new cancellation context so we can cancel in the case of an error
ctx, cancel := context.WithCancel(ctx)
// Run the server
log.Info("starting built-in server for local operations", "addr", ln.Addr().String())
go server.Run(server.WithContext(ctx),
server.WithLogger(log),
server.WithGRPC(ln),
server.WithImpl(impl),
server.WithMachineImpl(impl.(vagrant_plugin_sdk.MachineServiceServer)),
server.WithEnvironmentImpl(impl.(vagrant_plugin_sdk.ProjectServiceServer)),
)
client, err := serverclient.NewVagrantClient(ctx, log, ln.Addr().String())
if err != nil {
cancel()
return nil, err
}
// Setup our server config. The configuration is specifically set so
// so that there is no advertise address which will disable the CEB
// completely.
_, err = client.SetServerConfig(ctx, &vagrant_server.SetServerConfigRequest{
Config: &vagrant_server.ServerConfig{
AdvertiseAddrs: []*vagrant_server.ServerConfig_AdvertiseAddr{
{
Addr: "",
},
},
},
})
if err != nil {
cancel()
return nil, err
}
// Success, persist the closers
cleanupClosers := closers
closers = nil
b.cleanup(func() {
for _, c := range cleanupClosers {
c.Close()
}
// Force the ruby runtime to shut down
if cl, err := vrr.Client(); err == nil {
cl.Close()
}
})
_ = cancel // pacify vet lostcancel
return client.Conn(), nil
}
func (b *Basis) initVagrantRubyRuntime() (*plugin.Client, error) {
// TODO: Update for actual release usage. This is dev only now.
// TODO: We should also locate a free port on startup and use that port
_, this_dir, _, _ := runtime.Caller(0)
cmd := exec.Command(
"bundle", "exec", "vagrant", "serve",
)
cmd.Env = []string{
"BUNDLE_GEMFILE=" + filepath.Join(this_dir, "../../..", "Gemfile"),
"VAGRANT_I_KNOW_WHAT_IM_DOING_PLEASE_BE_QUIET=true",
"VAGRANT_LOG=debug",
"VAGRANT_LOG_FILE=/tmp/vagrant.log",
}
config := sr.RubyVagrantPluginConfig(b.logger)
config.Cmd = cmd
rubyServerClient := plugin.NewClient(config)
_, err := rubyServerClient.Start()
if err != nil {
return nil, err
}
return rubyServerClient, nil
}
// negotiateApiVersion negotiates the API version to use and validates
// that we are compatible to talk to the server.
func (b *Basis) negotiateApiVersion(ctx context.Context) error {
log := b.logger
log.Trace("requesting version info from server")
resp, err := b.client.GetVersionInfo(ctx, &empty.Empty{})
if err != nil {
return err
}
log.Info("server version info",
"version", resp.Info.Version,
"api_min", resp.Info.Api.Minimum,
"api_current", resp.Info.Api.Current,
"entrypoint_min", resp.Info.Entrypoint.Minimum,
"entrypoint_current", resp.Info.Entrypoint.Current,
)
vsn, err := protocolversion.Negotiate(protocolversion.Current().Api, resp.Info.Api)
if err != nil {
return err
}
log.Info("negotiated api version", "version", vsn)
return nil
}