// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package core import ( "context" "errors" "fmt" "os" "path/filepath" "strings" "sync" "time" "github.com/hashicorp/go-argmapper" "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-multierror" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/anypb" "github.com/hashicorp/vagrant-plugin-sdk/component" "github.com/hashicorp/vagrant-plugin-sdk/core" "github.com/hashicorp/vagrant-plugin-sdk/datadir" "github.com/hashicorp/vagrant-plugin-sdk/internal-shared/cacher" "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/server/proto/vagrant_server" "github.com/hashicorp/vagrant/internal/serverclient" ) const DEFAULT_COMMUNICATOR_NAME = "ssh" type Target struct { cache cacher.Cache // local target cache cleanup cleanup.Cleanup // cleanup tasks to be run on close client *serverclient.VagrantClient // client to vagrant server ctx context.Context // local target context dir *datadir.Target // data directory for target factory *Factory // scope factory jobInfo *component.JobInfo // jobInfo is the base job info for executed functions logger hclog.Logger // target specific logger project *Project // project which owns this target ready bool // flag that instance is ready target *vagrant_server.Target // stored target data ui terminal.UI // target UI vagrantfile *Vagrantfile // vagrantfile instance for target m sync.Mutex } func NewTarget(opts ...TargetOption) (*Target, error) { var t *Target var err error t = &Target{ cache: cacher.New(), cleanup: cleanup.New(), ctx: context.Background(), logger: hclog.L(), target: &vagrant_server.Target{}, } for _, fn := range opts { if optErr := fn(t); optErr != nil { err = multierror.Append(err, optErr) } } if err != nil { return nil, err } return t, err } func (t *Target) Init() error { var err error // If ready then Init was already run if t.ready { return nil } // Configure our logger t.logger = t.logger.ResetNamed("vagrant.core.target") // If no client is set, grab it from the project if t.client == nil && t.project != nil { t.client = t.project.client } // Attempt to reload the target to populate our // data. If the target is not found, create it. err = t.Reload() if err != nil { stat, ok := status.FromError(err) if !ok || stat.Code() != codes.NotFound { return err } // Target doesn't exist so save it to persist if err = t.Save(); err != nil { return err } } // If we don't have a project set, load it if t.project == nil { t.project, err = t.factory.NewProject(WithProjectRef(t.target.Project)) if err != nil { return fmt.Errorf("failed to load target project: %w", err) } } // Always ensure the project reference is set t.target.Project = t.project.Ref().(*vagrant_plugin_sdk.Ref_Project) // If the target directory is unset, set it if t.dir == nil { if t.dir, err = t.project.dir.Target(t.target.Name); err != nil { return err } } // If the ui is unset, use the project ui if t.ui == nil { t.ui = t.project.ui } // Save ourself when closed t.Closer(func() error { return t.Save() }) // If we have a vagrantfile set, we are done if t.vagrantfile != nil { t.vagrantfile.logger = t.logger.Named("vagrantfile") return nil } // We don't have a vagrantfile set so we need to restore // our stored configuration. First, make sure we have some // store configuration! if t.target.Configuration == nil { t.target.Configuration = &vagrant_plugin_sdk.Args_ConfigData{} // Since we don't have any data to load, just stub and return t.vagrantfile = t.project.vagrantfile.clone("target") t.vagrantfile.root = &component.ConfigData{ Data: map[string]interface{}{}, } return nil } v := t.project.vagrantfile.clone("target") v.logger = t.logger.Named("vagrantfile") if err = v.loadToRoot(t.target.Configuration); err != nil { return err } t.vagrantfile = v // Set flag that this instance is setup t.ready = true // Include this target information in log lines t.logger = t.logger.With("target", t) t.logger.Info("target initialized") return nil } func (t *Target) String() string { return fmt.Sprintf("core.Target[basis: %s, project: %s, resource_id: %s, name: %s, address: %p]", t.project.basis.Name(), t.project.Name(), t.target.ResourceId, t.target.Name, t, ) } func (t *Target) Config() (c interface{}, err error) { // err = vconfig.DecodeConfiguration(b.target.Configuration.Serialized, c) return nil, fmt.Errorf("not implemented") } func (t *Target) GetUUID() (id string, err error) { return t.target.Uuid, nil } func (t *Target) SetUUID(id string) (err error) { t.target.Uuid = id return t.Save() } func (t *Target) UI() (terminal.UI, error) { return t.ui, nil } func (t *Target) Ref() interface{} { return &vagrant_plugin_sdk.Ref_Target{ ResourceId: t.target.GetResourceId(), Project: t.target.GetProject(), Name: t.target.GetName(), } } // Name implements core.Target func (t *Target) Name() (string, error) { return t.target.Name, nil } // SetName implements core.Target func (t *Target) SetName(value string) (err error) { t.target.Name = value return t.Save() } // Provider implements core.Target func (t *Target) Provider() (p core.Provider, err error) { i := t.cache.Get("provider") if i != nil { return i.(core.Provider), nil } providerName, err := t.ProviderName() if err != nil { return nil, err } if providerName == "" { return nil, errors.New("cannot fetch provider for target when provider name is blank") } provider, err := t.project.basis.component( t.ctx, component.ProviderType, providerName) if err != nil { return } p = provider.Value.(core.Provider) if err := seedPlugin(p, t); err != nil { return nil, err } t.cache.Register("provider", p) return } // ProviderName implements core.Target func (t *Target) ProviderName() (string, error) { if t.target.Provider != "" { return t.target.Provider, nil } p, err := t.project.DefaultProvider( &core.DefaultProviderOptions{ CheckUsable: true, MachineName: t.target.Name, }, ) if err != nil { return "", err } t.target.Provider = p return p, nil } // Communicate implements core.Target func (t *Target) Communicate() (c core.Communicator, err error) { i := t.cache.Get("communicator") if i != nil { c = i.(core.Communicator) return } communicatorName := "" rawCommunicatorName, err := t.vagrantfile.GetValue("vm", "communicator") // If there is an error getting the communicator, default to using the ssh communicator if err != nil { communicatorName = DEFAULT_COMMUNICATOR_NAME } if rawCommunicatorName == nil { communicatorName = DEFAULT_COMMUNICATOR_NAME } else { communicatorName, err = optionToString(rawCommunicatorName) if err != nil { return nil, err } } communicator, err := t.project.basis.component( t.ctx, component.CommunicatorType, communicatorName) if err != nil { return } c = communicator.Value.(core.Communicator) if err = seedPlugin(c, t); err != nil { t.logger.Error("failed to seed communicator plugin", "error", err, ) return } t.cache.Register("communicator", c) return } // UpdatedAt implements core.Target func (t *Target) UpdatedAt() (tm *time.Time, err error) { return } // Project implements core.Target func (t *Target) Project() (core.Project, error) { return t.project, nil } // Metadata implements core.Target func (t *Target) Metadata() (map[string]string, error) { return t.target.Metadata.Metadata, nil } // DataDir implements core.Target func (t *Target) DataDir() (*datadir.Target, error) { return t.dir, nil } // Returns if the target exists based on state func (t *Target) Exists() bool { s, _ := t.State() if s == core.NOT_CREATED || s == core.UNKNOWN { return false } return true } func (t *Target) State() (state core.State, err error) { switch t.target.State { case vagrant_server.Operation_UNKNOWN: state = core.UNKNOWN case vagrant_server.Operation_CREATED: state = core.CREATED case vagrant_server.Operation_DESTROYED: state = core.DESTROYED case vagrant_server.Operation_HALTED: state = core.HALTED case vagrant_server.Operation_NOT_CREATED: state = core.NOT_CREATED case vagrant_server.Operation_PENDING: state = core.PENDING default: state = core.UNKNOWN } return } // Record implements core.Target func (t *Target) Record() (*anypb.Any, error) { return t.target.Record, nil } // Specialize implements core.Target func (t *Target) Specialize(typ interface{}) (specialized interface{}, err error) { switch typ.(type) { case *core.Machine: specialized = t.Machine() } return } // Resource ID for this target func (t *Target) ResourceId() (string, error) { return t.target.ResourceId, nil } // Returns the job info if currently set func (t *Target) JobInfo() *component.JobInfo { return t.jobInfo } // Client returns the API client for the backend server. func (t *Target) Client() *serverclient.VagrantClient { return t.client } func (t *Target) Closer(c func() error) { t.cleanup.Do(c) } // Close is called to clean up resources allocated by the target. // This should be called and blocked on to gracefully stop the target. func (t *Target) Close() (err error) { t.logger.Debug("closing target") return t.cleanup.Close() } // Reload the target data func (t *Target) Reload() (err error) { t.m.Lock() defer t.m.Unlock() result, err := t.Client().FindTarget(t.ctx, &vagrant_server.FindTargetRequest{ Target: t.target, }, ) if err != nil { return } t.target = result.Target return } // Saves the target to the db func (t *Target) Save() (err error) { t.m.Lock() defer t.m.Unlock() t.logger.Debug("saving target to db") // If there were any modification to the configuration // after init, be sure we capture them if t.vagrantfile != nil { t.target.Configuration, err = t.vagrantfile.rootToStore() if err != nil { t.logger.Warn("failed to serialize configuration prior to save", "error", err, ) // Only warn since we want to save whatever information we can err = nil } } result, err := t.Client().UpsertTarget(t.ctx, &vagrant_server.UpsertTargetRequest{ Target: t.target}) if err != nil { t.logger.Trace("failed to save target", "error", err) return } t.target = result.Target return } func (t *Target) Destroy() (err error) { // Run all the cleanup tasks on the target t.m.Lock() defer t.m.Unlock() t.logger.Trace("destroying target") // Delete the target from the database _, err = t.Client().DeleteTarget(t.ctx, &vagrant_server.DeleteTargetRequest{ Target: t.Ref().(*vagrant_plugin_sdk.Ref_Target), }) // Remove all the files inside the datadir without wiping the datadir itself files, err := filepath.Glob(filepath.Join(t.dir.DataDir().String(), "*")) if err != nil { return err } for _, file := range files { rerr := os.RemoveAll(file) if rerr != nil { err = multierror.Append(err, rerr) } } return } func (t *Target) Run(ctx context.Context, task *vagrant_server.Job_CommandOp) (err error) { t.logger.Debug("running new command", "command", task) cmd, err := t.project.basis.component( ctx, component.CommandType, task.Component.Name) if err != nil { t.logger.Error("failed to build requested component", "type", component.CommandType, "name", task.Component.Name, "error", err) return } fn := cmd.Value.(component.Command).ExecuteFunc( strings.Split(task.Command, " ")) result, err := t.callDynamicFunc(ctx, t.logger, fn, (*int32)(nil), argmapper.Typed(task.CliArgs, t.jobInfo, t.dir, t.ctx, t.ui), argmapper.ConverterFunc(cmd.mappers...), ) if err != nil || result == nil || result.(int32) != 0 { t.logger.Error("failed to execute command", "type", component.CommandType, "name", task.Component.Name, "error", err) cmdErr := &runError{} if err != nil { cmdErr.err = err } if result != nil { cmdErr.exitCode = result.(int32) } return cmdErr } return } // Vagrantfile implements core.Target func (t *Target) Vagrantfile() (core.Vagrantfile, error) { return t.vagrantfile, nil } func (t *Target) seed(fn func(*core.Seeds)) { t.project.seed( func(s *core.Seeds) { s.AddNamed("target", t) s.AddNamed("target_ui", t.ui) s.AddTyped(t) if fn != nil { fn(s) } }, ) } // Specializes target into a machine func (t *Target) Machine() core.Machine { cm := t.cache.Get("machine") if cm != nil { return cm.(core.Machine) } targetMachine := &vagrant_server.Target_Machine{} t.target.Record.UnmarshalTo(targetMachine) m := &Machine{ Target: t, logger: t.logger.Named("machine"), machine: targetMachine, cache: cacher.New(), vagrantfile: t.vagrantfile, } t.Closer(func() error { return m.Save() }) t.cache.Register("machine", m) return m } // Calls the function provided and converts the // result to an expected type. If no type conversion // is required, a `false` value for the expectedType // will return the raw interface return value. // // By default, the target is added as a typed argument // and the target and target UI are both added as a // named arguments. Execution is passed up to the project // level so it can set arguments as well. func (t *Target) callDynamicFunc( ctx context.Context, // context for function execution log hclog.Logger, // logger to provide function execution f interface{}, // function to call expectedType interface{}, // nil pointer of expected return type args ...argmapper.Arg, // list of argmapper arguments ) (interface{}, error) { // ensure our UI status is closed after every call in case it is used defer t.ui.Status().Close() return t.project.callDynamicFunc(ctx, log, f, expectedType, args...) } func (t *Target) execHook( ctx context.Context, log hclog.Logger, h *config.Hook, ) error { return execHook(ctx, t, log, h) } func (t *Target) doOperation( ctx context.Context, log hclog.Logger, op operation, ) (interface{}, proto.Message, error) { return doOperation(ctx, log, t, op) } // Options type for target loading type TargetOption func(*Target) error func WithProject(p *Project) TargetOption { return func(t *Target) (err error) { t.project = p t.target.Project = p.Ref().(*vagrant_plugin_sdk.Ref_Project) return } } func WithTargetProjectRef(p *vagrant_plugin_sdk.Ref_Project) TargetOption { return func(t *Target) (err error) { t.target.Project = p return } } // Set a vagrantfile instance on target func WithTargetVagrantfile(v *Vagrantfile) TargetOption { return func(t *Target) (err error) { t.vagrantfile = v return } } // Set name on target func WithTargetName(name string) TargetOption { return func(t *Target) (err error) { t.target.Name = name return nil } } // Configure target with proto ref func WithTargetRef(r *vagrant_plugin_sdk.Ref_Target) TargetOption { return func(t *Target) (err error) { if r.Name != "" { t.target.Name = r.Name } if r.ResourceId != "" { t.target.ResourceId = r.ResourceId } if r.Project != nil { t.target.Project = r.Project } return } } func WithProvider(provider string) TargetOption { return func(t *Target) (err error) { if t != nil && t.target != nil && provider != "" { t.target.Provider = provider } return nil } } var _ core.Target = (*Target)(nil) var _ Scope = (*Target)(nil)