vaguerent/internal/core/target.go
2022-06-20 13:00:02 -07:00

651 lines
15 KiB
Go

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"
goplugin "github.com/hashicorp/go-plugin"
"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/dynamic"
"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/plugin"
"github.com/hashicorp/vagrant/internal/server/proto/vagrant_server"
"github.com/hashicorp/vagrant/internal/serverclient"
)
type Target struct {
ctx context.Context
target *vagrant_server.Target
project *Project
logger hclog.Logger
dir *datadir.Target
m sync.Mutex
jobInfo *component.JobInfo
closers []func() error
ui terminal.UI
cache cacher.Cache
vagrantfile *Vagrantfile
}
func (t *Target) String() string {
return fmt.Sprintf("core.Target[basis: %s, project: %s, resource_id: %s, address: %p]",
t.project.basis.Name(), t.project.Name(), t.target.ResourceId, 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.ResourceId,
Project: t.target.Project,
Name: t.target.Name,
}
}
// 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
}
// TODO: get the communicator name from the Vagrantfile
// eg. t.target.Configuration.ConfigVm.Communicator
communicatorName := "ssh"
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
}
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.project.basis.client
}
func (t *Target) Closer(c func() error) {
t.closers = append(t.closers, 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",
"target", t)
for _, c := range t.closers {
if cerr := c(); cerr != nil {
t.logger.Warn("error executing closer",
"error", cerr)
err = multierror.Append(err, cerr)
}
}
// Remove this target from built target list in project
delete(t.project.targets, t.target.Name)
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",
"target", t.target.ResourceId,
"name", t.target.Name,
)
result, err := t.Client().UpsertTarget(t.ctx, &vagrant_server.UpsertTargetRequest{
Target: t.target})
if err != nil {
t.logger.Trace("failed to save target",
"target", t.target.ResourceId,
"error", err)
return
}
t.target = result.Target
return
}
func (t *Target) Destroy() (err error) {
t.Close()
t.m.Lock()
defer t.m.Unlock()
_, 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.Task) (err error) {
t.logger.Debug("running new task",
"target", t,
"task", task)
// Intialize targets
if err = t.project.InitTargets(); err != nil {
return err
}
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.CommandName, " "))
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
}
// LoadTarget implements originScope
func (t *Target) LoadTarget(topts ...TargetOption) (*Target, error) {
return nil, fmt.Errorf("targets cannot be loaded from a target")
}
// Boxes implements originScope
func (t *Target) Boxes() (core.BoxCollection, error) {
return nil, fmt.Errorf("boxes cannot be loaded from a target")
}
// Vagrantfile implements originScope / core.Target
func (t *Target) Vagrantfile() (core.Vagrantfile, error) {
return t.vagrantfile, nil
}
// Cache implements originScope
func (t *Target) Cache() cacher.Cache {
return t.project.basis.cache
}
// Broker implements originScope
func (t *Target) Broker() *goplugin.GRPCBroker {
return t.project.basis.plugins.LegacyBroker()
}
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,
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)
}
// Initialize the target instance
func (t *Target) init() (err error) {
t.logger.Info("running init on target", "target", t.target.Name)
// Name or resource id is required for a target to be loaded
if t.target.Name == "" && t.target.ResourceId == "" {
return fmt.Errorf("cannot load a target without name or resource id")
}
// A parent project is also required
if t.project == nil {
return fmt.Errorf("cannot load a target without defined project")
}
// If the configuration was updated during load, save it so
// we can re-apply after loading stored data
var conf *vagrant_plugin_sdk.Args_ConfigData
if t.target != nil && t.target.Configuration != nil {
conf = t.target.Configuration
}
// First we want to run a lookup if this target already exists
if t.target.ResourceId != "" {
resp, err := t.Client().FindTarget(t.ctx,
&vagrant_server.FindTargetRequest{
Target: &vagrant_server.Target{
ResourceId: t.target.ResourceId,
},
},
)
if err != nil {
return err
}
t.target = resp.Target
} else {
for _, pt := range t.project.project.Targets {
if t.target.Name == pt.Name {
resp, err := t.Client().FindTarget(t.ctx,
&vagrant_server.FindTargetRequest{
Target: &vagrant_server.Target{
ResourceId: pt.ResourceId,
},
},
)
if err != nil {
return err
}
t.target = resp.Target
}
}
}
// If we have configuration data, re-apply it
if conf != nil {
t.target.Configuration = conf
}
// Set the project into the target
t.target.Project = t.project.Ref().(*vagrant_plugin_sdk.Ref_Project)
// If we have a vagrantfile attached, we're done
if t.vagrantfile != nil {
return
}
// If we don't have configuration data, just stub
if t.target.Configuration == nil {
t.target.Configuration = &vagrant_plugin_sdk.Args_ConfigData{}
return
}
t.logger.Info("vagrantfile has not been defined so generating from store config",
"name", t.target.Name,
)
internal := plugin.NewInternal(
t.project.basis.plugins.LegacyBroker(),
t.project.basis.cache,
t.project.basis.cleaner,
t.logger,
t.project.basis.mappers,
)
// Load the configuration data we have
raw, err := dynamic.Map(
t.target.Configuration,
(**component.ConfigData)(nil),
argmapper.ConverterFunc(t.project.basis.mappers...),
argmapper.Typed(
t.ctx,
t.logger,
internal,
),
)
if err != nil {
return
}
t.vagrantfile = t.project.vagrantfile.clone("target", t)
t.vagrantfile.root = raw.(*component.ConfigData)
return
}
// Options type for target loading
type TargetOption func(*Target) error
// Set a vagrantfile instance on target
func WithTargetVagrantfile(v *Vagrantfile) TargetOption {
return func(t *Target) (err error) {
t.vagrantfile = v
t.target.Configuration, err = v.rootToStore()
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) {
// Project must be set before we continue
if t.project == nil {
return fmt.Errorf("project must be set before loading target")
}
// Target ref must include a resource id or name
if r.Name == "" && r.ResourceId == "" {
return fmt.Errorf("target ref must include ResourceId and/or Name")
}
var target *vagrant_server.Target
result, err := t.Client().FindTarget(t.ctx,
&vagrant_server.FindTargetRequest{
Target: &vagrant_server.Target{
ResourceId: r.ResourceId,
Name: r.Name,
Project: t.project.Ref().(*vagrant_plugin_sdk.Ref_Project),
},
},
)
// TODO(spox): check for not found and error if something different
// if err != nil {
// return err
// }
if result != nil {
target = result.Target
} else {
var result *vagrant_server.UpsertTargetResponse
result, err = t.Client().UpsertTarget(t.ctx,
&vagrant_server.UpsertTargetRequest{
Target: &vagrant_server.Target{
Name: r.Name,
Project: t.project.Ref().(*vagrant_plugin_sdk.Ref_Project),
},
},
)
if err != nil {
return
}
target = result.Target
}
if r.Project != nil && target.Project.ResourceId != r.Project.ResourceId {
t.logger.Error("invalid project for target",
"request-project", r.Project,
"target-project", target.Project)
return fmt.Errorf("target project configuration is invalid")
}
t.target = target
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)