vaguerent/internal/core/machine.go
Paul Hinze 91478e9e0a
Fix method for getting default synced folder type
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.
2022-07-07 11:24:53 -05:00

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)