298 lines
8.2 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package client
import (
"context"
"fmt"
"io/fs"
"net"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/glebarez/sqlite"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/vagrant-plugin-sdk/helper/paths"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/emptypb"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"github.com/hashicorp/vagrant/internal/protocolversion"
"github.com/hashicorp/vagrant/internal/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 (c *Client) initServerClient(ctx context.Context, cfg *clientConfig) (*grpc.ClientConn, error) {
log := c.logger.ResetNamed("vagrant.server")
// If we're local, then connection is optional.
opts := cfg.connectOpts
if true { // c.localServer {
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")
c.Cleanup(func() error { return 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 c.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 (c *Client) initLocalServer(ctx context.Context) (_ *grpc.ClientConn, err error) {
log := c.logger.ResetNamed("vagrant.server")
c.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 cleanups []func() error
// If we encounter an error force all the
// local cleanups to run
defer func() {
if err != nil {
for _, c := range cleanups {
c()
}
}
}()
dataPath, err := paths.VagrantData()
if err != nil {
return
}
path := dataPath.Join("data.db").String()
log.Debug("opening local mode DB", "path", path)
// Open our database
db, err := gorm.Open(sqlite.Open(path), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
db.Exec("PRAGMA foreign_keys = ON")
if err != nil {
return
}
// Create our server
impl, err := singleprocess.New(
singleprocess.WithDB(db),
singleprocess.WithLogger(log.Named("singleprocess")),
)
if err != nil {
log.Trace("failed singleprocess server setup", "error", err)
return
}
// Prune old jobs before closing the database
cleanups = append(cleanups, func() error {
_, err := impl.PruneOldJobs(ctx, nil)
return 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
}
cleanups = append(cleanups, func() error { return ln.Close() })
// Create a new cancellation context so we can cancel in the case of an error
ctx, cancel := context.WithCancel(ctx)
defer func() {
if err != nil {
cancel()
}
}()
// 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),
)
client, err := serverclient.NewVagrantClient(ctx, log, ln.Addr().String())
if err != nil {
return
}
// Have the defined cleanups run when the basis is closed
for _, fn := range cleanups {
c.Cleanup(fn)
}
_ = cancel // pacify vet lostcancel
return client.Conn(), nil
}
// initVagrantRubyRuntime launches legacy vagrant as a gRPC server using the
// "serve" command.
//
// NOTE: We are assuming that the first executable we find in $PATH that is not
// _us_ is the legacy vagrant executable. It's up the packaging to ensure
// that is how things are set up.
func (c *Client) initVagrantRubyRuntime() (rubyRuntime plugin.ClientProtocol, err error) {
var vagrantPath string
vagrantPath, err = lookPathSkippingSelf("vagrant")
if err != nil {
return
}
config := serverclient.RubyVagrantPluginConfig(c.logger)
config.Cmd = exec.Command(vagrantPath, "serve")
rc := plugin.NewClient(config)
if _, err = rc.Start(); err != nil {
return
}
if rubyRuntime, err = rc.Client(); err != nil {
return
}
// Send a request to the vagrant ruby runtime to shut itself down
// NOTE: Closing the client when using the official package will not
// stop the process. This is because the package uses a custom
// wrapper for starting Vagrant which results in a different
// PID than what is originally started.
c.Cleanup(func() error {
vr, err := rubyRuntime.Dispense("vagrantrubyruntime")
if err != nil {
c.logger.Error("failed to dispense the vagrant ruby runtime",
"error", err,
)
return err
}
vrc, ok := vr.(serverclient.RubyVagrantClient)
if !ok {
c.logger.Error("dispensed value is not a ruby runtime client")
return fmt.Errorf("dispensed value is not a ruby vagrant client (%T)", vr)
}
return vrc.Stop()
})
// Close the ruby runtime client.
c.Cleanup(func() error {
return rubyRuntime.Close()
})
return
}
// negotiateApiVersion negotiates the API version to use and validates
// that we are compatible to talk to the server.
func (c *Client) negotiateApiVersion(ctx context.Context) error {
c.logger.Trace("requesting version info from server")
resp, err := c.client.GetVersionInfo(ctx, &emptypb.Empty{})
if err != nil {
return err
}
c.logger.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
}
c.logger.Info("negotiated api version", "version", vsn)
return nil
}
// lookPathSkippingSelf is a copy of exec.LookPath modified to skip the
// currently running executable.
func lookPathSkippingSelf(file string) (string, error) {
myselfPath, err := os.Executable()
if err != nil {
return "", err
}
myself, err := os.Stat(myselfPath)
if err != nil {
return "", err
}
if strings.Contains(file, "/") {
err := findExecutable(file, myself)
if err == nil {
return file, nil
}
return "", &exec.Error{Name: file, Err: err}
}
path := os.Getenv("PATH")
for _, dir := range filepath.SplitList(path) {
if dir == "" {
// Unix shell semantics: path element "" means "."
dir = "."
}
path := filepath.Join(dir, file)
if err := findExecutable(path, myself); err == nil {
return path, nil
}
}
return "", &exec.Error{Name: file, Err: exec.ErrNotFound}
}
// findExecutableSkippingSelf is a copy of exec.findExecutable modified to skip
// the provided FileInfo. It's used to power lookPathSkippingSelf.
func findExecutable(file string, skip os.FileInfo) error {
d, err := os.Stat(file)
if err != nil {
return err
}
if os.SameFile(d, skip) {
return fs.ErrPermission
}
if m := d.Mode(); !m.IsDir() && m&0111 != 0 {
return nil
}
return fs.ErrPermission
}