// 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 }