As a part of a series of larger changes the default synced folder type accidentally got wired up to DefaultProvider instead of its dedicated defaultSyncedFolderType() method. This was working fine for "virtualbox" where the provider name and the synced folder name are both the same, but it was causing virtualbox synced folders to be selected when using the "docker" provider and making things break. This is one necessary step to get machine lifecycles working again with Docker.
529 lines
12 KiB
Go
529 lines
12 KiB
Go
package core
|
|
|
|
import (
|
|
"fmt"
|
|
"os/user"
|
|
"reflect"
|
|
"sort"
|
|
|
|
"github.com/mitchellh/mapstructure"
|
|
"google.golang.org/protobuf/types/known/anypb"
|
|
|
|
"github.com/hashicorp/go-hclog"
|
|
"github.com/hashicorp/vagrant-plugin-sdk/component"
|
|
"github.com/hashicorp/vagrant-plugin-sdk/core"
|
|
"github.com/hashicorp/vagrant-plugin-sdk/helper/path"
|
|
"github.com/hashicorp/vagrant-plugin-sdk/helper/types"
|
|
"github.com/hashicorp/vagrant-plugin-sdk/internal-shared/cacher"
|
|
"github.com/hashicorp/vagrant-plugin-sdk/proto/vagrant_plugin_sdk"
|
|
"github.com/hashicorp/vagrant/internal/server/proto/vagrant_server"
|
|
)
|
|
|
|
type Machine struct {
|
|
*Target
|
|
box *Box
|
|
machine *vagrant_server.Target_Machine
|
|
logger hclog.Logger
|
|
cache cacher.Cache
|
|
vagrantfile *Vagrantfile
|
|
}
|
|
|
|
// Close implements core.Machine
|
|
func (m *Machine) Close() (err error) {
|
|
return
|
|
}
|
|
|
|
// ID implements core.Machine
|
|
func (m *Machine) ID() (id string, err error) {
|
|
return m.machine.Id, nil
|
|
}
|
|
|
|
// SetID implements core.Machine
|
|
func (m *Machine) SetID(value string) (err error) {
|
|
m.machine.Id = value
|
|
|
|
// Also set uid
|
|
user, err := user.Current()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
m.machine.Uid = user.Uid
|
|
|
|
// Persist changes
|
|
if value == "" {
|
|
m.target.Record = nil
|
|
err = m.Destroy()
|
|
} else {
|
|
err = m.SaveMachine()
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (m *Machine) Box() (b core.Box, err error) {
|
|
if m.box == nil {
|
|
boxes, _ := m.project.Boxes()
|
|
boxName, err := m.vagrantfile.GetValue("vm", "box")
|
|
if err != nil {
|
|
m.logger.Error("failed to get machine box name from config",
|
|
"error", err,
|
|
)
|
|
|
|
return nil, err
|
|
}
|
|
provider, err := m.ProviderName()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
b, err := boxes.Find(boxName.(string), "", provider)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if b == nil {
|
|
return &Box{
|
|
basis: m.project.basis,
|
|
box: &vagrant_server.Box{
|
|
Name: boxName.(string),
|
|
Provider: provider,
|
|
},
|
|
}, nil
|
|
}
|
|
m.machine.Box = b.(*Box).ToProto()
|
|
m.SaveMachine()
|
|
m.box = b.(*Box)
|
|
}
|
|
|
|
return m.box, nil
|
|
}
|
|
|
|
// Guest implements core.Machine
|
|
func (m *Machine) Guest() (g core.Guest, err error) {
|
|
comm, err := m.Communicate()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
isReady, err := comm.Ready(m)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !isReady {
|
|
return nil, fmt.Errorf("unable to communicate with guest")
|
|
}
|
|
defer func() {
|
|
if g != nil {
|
|
err = seedPlugin(g, m)
|
|
if err == nil {
|
|
m.cache.Register("guest", g)
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Note that if we get the guest value from the
|
|
// local cache, we return it directly to prevent
|
|
// the seeding and cache registration from happening
|
|
// again.
|
|
i := m.cache.Get("guest")
|
|
if i != nil {
|
|
return i.(core.Guest), nil
|
|
}
|
|
|
|
// Check if a guest is provided by the Vagrantfile. If it is, then try
|
|
// to use that guest
|
|
vg, err := m.vagrantfile.GetValue("vm", "guest")
|
|
if err != nil {
|
|
m.logger.Trace("failed to get guest value from vagrantfile",
|
|
"error", err,
|
|
)
|
|
} else {
|
|
guestName, ok := vg.(string)
|
|
if ok {
|
|
var guest *Component
|
|
guest, err = m.project.basis.component(m.ctx, component.GuestType, guestName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
g = guest.Value.(core.Guest)
|
|
return
|
|
} else {
|
|
m.logger.Debug("guest name was not a valid string value",
|
|
"guest", vg,
|
|
)
|
|
}
|
|
|
|
}
|
|
|
|
// Try to detect a guest
|
|
guests, err := m.project.basis.typeComponents(m.ctx, component.GuestType)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
names := make([]string, 0, len(guests))
|
|
pcount := map[string]int{}
|
|
|
|
for name, g := range guests {
|
|
names = append(names, name)
|
|
pcount[name] = g.plugin.ParentCount()
|
|
}
|
|
|
|
sort.Slice(names, func(i, j int) bool {
|
|
in := names[i]
|
|
jn := names[j]
|
|
// TODO check values exist before use
|
|
return pcount[in] > pcount[jn]
|
|
})
|
|
|
|
for _, name := range names {
|
|
guest := guests[name].Value.(core.Guest)
|
|
detected, gerr := guest.Detect(m.toTarget())
|
|
if gerr != nil {
|
|
m.logger.Error("guest error on detection check",
|
|
"plugin", name,
|
|
"type", "Guest",
|
|
"error", err)
|
|
|
|
continue
|
|
}
|
|
if detected {
|
|
m.logger.Info("guest detection complete",
|
|
"name", name,
|
|
)
|
|
g = guest
|
|
return
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("failed to detect guest plugin for current platform")
|
|
}
|
|
|
|
func (m *Machine) Inspect() (printable string, err error) {
|
|
name, err := m.Name()
|
|
provider, err := m.Provider()
|
|
printable = "#<" + reflect.TypeOf(m).String() + ": " + name + " (" + reflect.TypeOf(provider).String() + ")>"
|
|
return
|
|
}
|
|
|
|
// ConnectionInfo implements core.Machine
|
|
func (m *Machine) ConnectionInfo() (info *core.ConnectionInfo, err error) {
|
|
// TODO: need Vagrantfile
|
|
return
|
|
}
|
|
|
|
// MachineState implements core.Machine
|
|
func (m *Machine) MachineState() (state *core.MachineState, err error) {
|
|
p, err := m.Provider()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return p.State()
|
|
}
|
|
|
|
// SetMachineState implements core.Machine
|
|
func (m *Machine) SetMachineState(state *core.MachineState) (err error) {
|
|
var st *vagrant_plugin_sdk.Args_Target_Machine_State
|
|
mapstructure.Decode(state, &st)
|
|
m.machine.State = st
|
|
|
|
switch st.Id {
|
|
case "not_created":
|
|
m.target.State = vagrant_server.Operation_UNKNOWN
|
|
case "running":
|
|
m.target.State = vagrant_server.Operation_CREATED
|
|
case "poweroff":
|
|
m.target.State = vagrant_server.Operation_DESTROYED
|
|
case "pending":
|
|
m.target.State = vagrant_server.Operation_PENDING
|
|
default:
|
|
m.target.State = vagrant_server.Operation_UNKNOWN
|
|
}
|
|
|
|
return m.SaveMachine()
|
|
}
|
|
|
|
func (m *Machine) UID() (userId string, err error) {
|
|
return m.machine.Uid, nil
|
|
}
|
|
|
|
func StringToPathFunc() mapstructure.DecodeHookFunc {
|
|
return func(
|
|
f reflect.Type,
|
|
t reflect.Type,
|
|
data interface{}) (interface{}, error) {
|
|
if f.Kind() != reflect.String {
|
|
return data, nil
|
|
}
|
|
if !t.Implements(reflect.TypeOf((*path.Path)(nil)).Elem()) {
|
|
return data, nil
|
|
}
|
|
|
|
// Convert it
|
|
return path.NewPath(data.(string)), nil
|
|
}
|
|
}
|
|
|
|
func (m *Machine) defaultSyncedFolderType() (folderType string, err error) {
|
|
logger := m.logger.Named("default-synced-folder-type")
|
|
|
|
// Get all available synced folder plugins
|
|
sfPlugins, err := m.project.basis.plugins.ListPlugins("synced_folder")
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Load full plugins so we can read options
|
|
for i, sfp := range sfPlugins {
|
|
fullPlugin, err := m.project.basis.plugins.GetPlugin(sfp.Name, sfp.Type)
|
|
if err != nil {
|
|
logger.Error("error while loading synced folder plugin: name", sfp.Name, "err", err)
|
|
return "", fmt.Errorf("error while loading synced folder plugin: name = %s, err = %s", sfp.Name, err)
|
|
}
|
|
sfPlugins[i] = fullPlugin
|
|
}
|
|
|
|
// Sort by plugin priority. Higher is first
|
|
sort.SliceStable(sfPlugins, func(i, j int) bool {
|
|
iPriority := sfPlugins[i].Options.(*component.SyncedFolderOptions).Priority
|
|
jPriority := sfPlugins[j].Options.(*component.SyncedFolderOptions).Priority
|
|
return iPriority > jPriority
|
|
})
|
|
|
|
logger.Debug("sorted synced folder plugins", "names", sfPlugins)
|
|
|
|
allowedTypesRaw, err := m.vagrantfile.GetValue("vm", "allowed_synced_folder_types")
|
|
if err != nil {
|
|
m.logger.Warn("failed to fetch allowed synced folder types, ignoring",
|
|
"error", err,
|
|
)
|
|
err = nil
|
|
} else {
|
|
allowedTypes, ok := allowedTypesRaw.([]interface{})
|
|
if !ok {
|
|
m.logger.Warn("unexpected type for allowed synced folder types",
|
|
"type", hclog.Fmt("%T", allowedTypesRaw),
|
|
)
|
|
}
|
|
// Remove unallowed types
|
|
if len(allowedTypes) > 0 {
|
|
allowed := make(map[string]struct{})
|
|
for _, a := range allowedTypes {
|
|
typ, err := optionToString(a)
|
|
if err != nil {
|
|
m.logger.Warn("failed to convert synced folder type to string",
|
|
"type", hclog.Fmt("%T", a),
|
|
)
|
|
}
|
|
allowed[typ] = struct{}{}
|
|
}
|
|
k := 0
|
|
for _, sfp := range sfPlugins {
|
|
if _, ok := allowed[sfp.Name]; ok {
|
|
sfPlugins[k] = sfp
|
|
k++
|
|
} else {
|
|
logger.Debug("removing disallowed plugin", "type", sfp.Name)
|
|
}
|
|
}
|
|
sfPlugins = sfPlugins[:k]
|
|
}
|
|
}
|
|
// Check for first usable plugin
|
|
for _, sfp := range sfPlugins {
|
|
syncedFolder := sfp.Plugin.(core.SyncedFolder)
|
|
usable, err := syncedFolder.Usable(m)
|
|
if err != nil {
|
|
logger.Error("error on usable check", "plugin", sfp.Name, "error", err)
|
|
continue
|
|
}
|
|
if usable {
|
|
logger.Info("returning default", "name", sfp.Name)
|
|
return sfp.Name, nil
|
|
} else {
|
|
logger.Debug("skipping unusable plugin", "name", sfp.Name)
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("failed to detect guest plugin for current platform")
|
|
}
|
|
|
|
// SyncedFolders implements core.Machine
|
|
func (m *Machine) SyncedFolders() (folders []*core.MachineSyncedFolder, err error) {
|
|
syncedFoldersRaw, err := m.vagrantfile.GetValue("vm", "__synced_folders")
|
|
if err != nil {
|
|
m.logger.Error("failed to load synced folders",
|
|
"error", err,
|
|
)
|
|
|
|
return
|
|
}
|
|
tmpFolders, ok := syncedFoldersRaw.(map[interface{}]interface{})
|
|
if !ok {
|
|
m.logger.Error("synced folders configuration is unexpected type",
|
|
"type", hclog.Fmt("%T", syncedFoldersRaw),
|
|
)
|
|
return nil, fmt.Errorf("invalid configuration type for synced folders")
|
|
}
|
|
|
|
syncedFolders := map[string]map[interface{}]interface{}{}
|
|
|
|
for k, v := range tmpFolders {
|
|
var key string
|
|
var ok bool
|
|
if key, ok = k.(string); !ok {
|
|
if skey, ok := k.(types.Symbol); ok {
|
|
key = string(skey)
|
|
} else {
|
|
m.logger.Error("invalid key type for synced folders",
|
|
"key", k,
|
|
"type", hclog.Fmt("%T", k),
|
|
)
|
|
|
|
return nil, fmt.Errorf("invalid configuration type for synced folder key")
|
|
}
|
|
}
|
|
value, ok := v.(map[interface{}]interface{})
|
|
if !ok {
|
|
m.logger.Error("invalid value type for synced folders",
|
|
"type", hclog.Fmt("%T", v),
|
|
)
|
|
}
|
|
|
|
syncedFolders[key] = value
|
|
}
|
|
|
|
for _, options := range syncedFolders {
|
|
var ftype string
|
|
typeRaw, ok := getOptionValue("type", options)
|
|
if ok {
|
|
if ftype, err = optionToString(typeRaw); err != nil {
|
|
m.logger.Debug("failed to convert folder type to string",
|
|
"error", err,
|
|
)
|
|
|
|
return
|
|
}
|
|
}
|
|
if ftype == "" {
|
|
ftype, err = m.defaultSyncedFolderType()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
lookup := "syncedfolder_" + ftype
|
|
v := m.cache.Get(lookup)
|
|
if v == nil {
|
|
plg, err := m.project.basis.component(m.ctx, component.SyncedFolderType, ftype)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
v = plg.Value.(core.SyncedFolder)
|
|
m.cache.Register(lookup, v)
|
|
}
|
|
|
|
if err = seedPlugin(v, m); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var guestPath, hostPath path.Path
|
|
guestPathRaw, ok := getOptionValue("guestpath", options)
|
|
if !ok {
|
|
return nil, fmt.Errorf("synced folder options do not include guest path value")
|
|
}
|
|
hostPathRaw, ok := getOptionValue("hostpath", options)
|
|
if !ok {
|
|
return nil, fmt.Errorf("synced folder options do not include host path value")
|
|
}
|
|
if gps, err := optionToString(guestPathRaw); err == nil {
|
|
guestPath = path.NewPath(gps)
|
|
} else {
|
|
return nil, err
|
|
}
|
|
if hps, err := optionToString(hostPathRaw); err == nil {
|
|
hostPath = path.NewPath(hps)
|
|
} else {
|
|
return nil, err
|
|
}
|
|
|
|
opts := map[string]interface{}{}
|
|
for k, v := range options {
|
|
key, err := optionToString(k)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
opts[key] = v
|
|
}
|
|
|
|
f := &core.Folder{
|
|
Source: hostPath,
|
|
Destination: guestPath,
|
|
Options: opts,
|
|
}
|
|
|
|
folders = append(folders, &core.MachineSyncedFolder{
|
|
Plugin: v.(core.SyncedFolder),
|
|
Folder: f,
|
|
})
|
|
}
|
|
return
|
|
}
|
|
|
|
func (m *Machine) SaveMachine() (err error) {
|
|
m.logger.Debug("saving machine to db", "machine", m.machine.Id)
|
|
// Update the target record and uuid to match the machine's new state
|
|
m.target.Record, err = anypb.New(m.machine)
|
|
m.target.Uuid = m.machine.Id
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return m.Save()
|
|
}
|
|
|
|
func (m *Machine) toTarget() core.Target {
|
|
return m
|
|
}
|
|
|
|
// Get option value from config map. Since keys in the config
|
|
// can be either string or types.Symbol, this helper function
|
|
// will check for either type being set
|
|
func getOptionValue(
|
|
name string, // name of option
|
|
options map[interface{}]interface{}, // options map from config
|
|
) (interface{}, bool) {
|
|
var key interface{}
|
|
key = name
|
|
result, ok := options[key]
|
|
if ok {
|
|
return result, true
|
|
}
|
|
key = types.Symbol(name)
|
|
result, ok = options[key]
|
|
if ok {
|
|
return result, true
|
|
}
|
|
|
|
return nil, false
|
|
}
|
|
|
|
// Option values from the config which are expected to be string
|
|
// values may be a string or types.Symbol. This helper function
|
|
// will take the value and convert it into a string if possible.
|
|
func optionToString(
|
|
opt interface{}, // value to convert
|
|
) (result string, err error) {
|
|
result, ok := opt.(string)
|
|
if ok {
|
|
return
|
|
}
|
|
|
|
sym, ok := opt.(types.Symbol)
|
|
if !ok {
|
|
return result, fmt.Errorf("option value is not string type (%T)", opt)
|
|
}
|
|
result = string(sym)
|
|
|
|
return
|
|
}
|
|
|
|
var _ core.Machine = (*Machine)(nil)
|
|
var _ core.Target = (*Machine)(nil)
|