vaguerent/internal/core/machine.go
Chris Roberts e958c6183a Adds initial HCP config support
Adds initial basic support for HCP based configuration in vagrant-go.
The initalization process has been updated to remove Vagrantfile parsing
from the client, moving it to the runner using init jobs for the basis
and the project (if there is one). Detection is done on the file based
on extension for Ruby based parsing or HCP based parsing.

Current HCP parsing is extremely simple and currently just a base to
build off. Config components will be able to implement an `Init`
function to handle receiving configuration data from a non-native source
file. This will be extended to include a default approach for injecting
defined data in the future.

Some cleanup was done in the state around validations. Some logging
adjustments were applied on the Ruby side for better behavior
consistency.

VirtualBox provider now caches locale detection to prevent multiple
checks every time the driver is initialized.
2023-09-07 17:26:10 -07:00

518 lines
12 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
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
}
func (m *Machine) String() string {
return fmt.Sprintf("core.Machine[basis: %s, project: %s, resource_id: %s, name: %s, address: %p]",
m.project.basis.Name(), m.project.Name(), m.target.ResourceId, m.target.Name, m,
)
}
// 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
m.target.State = vagrant_server.Operation_NOT_CREATED
}
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
}
if boxName == nil {
m.logger.Debug("vagrantfile has no box, so returning nil")
return nil, nil
}
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
}
m.logger.Info("have machine provider plugin, getting state")
s, err := p.State()
m.logger.Info("provider state is returned",
"state", s,
"error", err,
)
return s, err
}
// SetMachineState implements core.Machine
func (m *Machine) SetMachineState(state *core.MachineState) (err error) {
var st *vagrant_plugin_sdk.Args_Target_Machine_State
if err := mapstructure.Decode(state, &st); err != nil {
return err
}
m.machine.State = st
switch st.Id {
case "not_created":
m.target.State = vagrant_server.Operation_NOT_CREATED
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[interface{}]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) AsTarget() (core.Target, error) {
return m.Target, nil
}
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
// TODO(spox): the uuid shouldn't be getting set to the Id, was there any reason for this?
// m.target.Uuid = m.machine.Id
m.target.Record, err = anypb.New(m.machine)
if err != nil {
m.logger.Warn("failed to convert machine data to any value",
"error", err,
)
return
}
return m.Save()
}
func (m *Machine) toTarget() core.Target {
return m
}
var _ core.Machine = (*Machine)(nil)
var _ core.Target = (*Machine)(nil)
var _ Scope = (*Machine)(nil)