Legacy's `Machine#id=()` has an important side effect when a nil ID is specified - it clears the contents of the machine's DataDir. We mirrored this behavior over to gogo, with a subtle difference - we deleted the whole DataDir vs just its children. It turns out the Docker provider relies on the DataDir being cleared-but-not-removed by doing a SetId dance in its InitState action. (see 1e6259dd00d702f83048c75c5c229ce6494c4c6e). So here we need to mirror that behavior in order for the Docker provider to work properly.
488 lines
11 KiB
Go
488 lines
11 KiB
Go
package core
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/golang/protobuf/proto"
|
|
"github.com/golang/protobuf/ptypes"
|
|
"github.com/hashicorp/go-argmapper"
|
|
"github.com/hashicorp/go-hclog"
|
|
"github.com/hashicorp/go-multierror"
|
|
"github.com/hashicorp/vagrant/internal/config"
|
|
"github.com/hashicorp/vagrant/internal/serverclient"
|
|
"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/proto/vagrant_plugin_sdk"
|
|
"github.com/hashicorp/vagrant-plugin-sdk/terminal"
|
|
|
|
"github.com/hashicorp/vagrant/internal/server/proto/vagrant_server"
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
func (b *Target) Config() *vagrant_plugin_sdk.Vagrantfile_MachineConfig {
|
|
return b.target.Configuration
|
|
}
|
|
|
|
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) {
|
|
defer func() {
|
|
if p != nil {
|
|
err = seedPlugin(p, t)
|
|
if err == nil {
|
|
t.cache.Register("provider", p)
|
|
}
|
|
}
|
|
}()
|
|
i := t.cache.Get("provider")
|
|
if i != nil {
|
|
p = i.(core.Provider)
|
|
return
|
|
}
|
|
|
|
providerName, err := t.ProviderName()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
provider, err := t.project.basis.component(
|
|
t.ctx, component.ProviderType, providerName)
|
|
|
|
if err != nil {
|
|
return
|
|
}
|
|
p = provider.Value.(core.Provider)
|
|
|
|
return
|
|
}
|
|
|
|
// ProviderName implements core.Target
|
|
// TODO: Use actual value once provider is set on the go side
|
|
func (t *Target) ProviderName() (string, error) {
|
|
// return t.target.Provider, nil
|
|
return "virtualbox", 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)
|
|
}
|
|
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
|
|
}
|
|
|
|
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{}
|
|
ptypes.UnmarshalAny(t.target.Record, targetMachine)
|
|
m := &Machine{
|
|
Target: t,
|
|
logger: t.logger,
|
|
machine: targetMachine,
|
|
cache: cacher.New(),
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
type TargetOption func(*Target) error
|
|
|
|
func WithTargetName(name string) TargetOption {
|
|
return func(t *Target) (err error) {
|
|
if ex, _ := t.project.Target(name); ex != nil {
|
|
if et, ok := ex.(*Target); ok {
|
|
t.target = et.target
|
|
}
|
|
return
|
|
}
|
|
for _, target := range t.project.targets {
|
|
if target.target.Name != name {
|
|
continue
|
|
}
|
|
var result *vagrant_server.GetTargetResponse
|
|
result, err = t.Client().GetTarget(t.ctx,
|
|
&vagrant_server.GetTargetRequest{
|
|
Target: target.Ref().(*vagrant_plugin_sdk.Ref_Target)})
|
|
if err != nil {
|
|
return
|
|
}
|
|
t.target = result.Target
|
|
return
|
|
}
|
|
return fmt.Errorf("target `%s' is not registered in project", name)
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
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: r.Project,
|
|
},
|
|
},
|
|
)
|
|
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{
|
|
ResourceId: r.ResourceId,
|
|
Name: r.Name,
|
|
Project: r.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
|
|
}
|
|
}
|
|
|
|
var _ core.Target = (*Target)(nil)
|