vaguerent/internal/core/vagrantfile.go
Paul Hinze da77316f9a
Fix virtualbox lifecycle acctests
Two fixes:

 1. Allow us to make it through Vagrant::Environment initialization when
    there's no project yet, fixing box add and init commands.
 2. Don't prune UNKNOWN targets from state... as VBox looks like it
    leaves things in that state when it runs suspend.
2022-07-13 18:36:11 -05:00

1244 lines
30 KiB
Go

package core
import (
"context"
"fmt"
"sync"
"github.com/hashicorp/go-argmapper"
"github.com/hashicorp/go-hclog"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"github.com/hashicorp/vagrant-plugin-sdk/component"
"github.com/hashicorp/vagrant-plugin-sdk/core"
"github.com/hashicorp/vagrant-plugin-sdk/helper/types"
"github.com/hashicorp/vagrant-plugin-sdk/internal-shared/cacher"
"github.com/hashicorp/vagrant-plugin-sdk/internal-shared/cleanup"
"github.com/hashicorp/vagrant-plugin-sdk/internal-shared/dynamic"
"github.com/hashicorp/vagrant-plugin-sdk/internal-shared/protomappers"
"github.com/hashicorp/vagrant-plugin-sdk/proto/vagrant_plugin_sdk"
"github.com/hashicorp/vagrant/internal/plugin"
"github.com/hashicorp/vagrant/internal/server/proto/vagrant_server"
"github.com/hashicorp/vagrant/internal/serverclient"
)
// LoadLocation type defines the origin of a Vagrantfile. The
// higher the value of the LoadLocation, the higher the precedence
// when merging
type LoadLocation uint8
// DEFAULT_VM_NAME is the name that a target gets when none has been specified.
const DEFAULT_VM_NAME = "default"
const (
VAGRANTFILE_BOX LoadLocation = iota // Box
VAGRANTFILE_BASIS // Basis
VAGRANTFILE_PROJECT // Project
VAGRANTFILE_TARGET // Target
VAGRANTFILE_PROVIDER // Provider
)
// These are the locations which can be used for
// populating the root value in the vagrantfile
var ValidRootLocations = map[LoadLocation]struct{}{
VAGRANTFILE_BASIS: {},
VAGRANTFILE_PROJECT: {},
VAGRANTFILE_TARGET: {},
}
// Registration entry for a config component
type registration struct {
identifier string // The identifier is the root key used in the Vagrantfile
plugin *plugin.Plugin // Plugin that provides this config component
set bool // Flag to identify this configuration is in use
subregistrations map[string]*subregistration // Configuration plugins used within this configuration
}
func (r *registration) String() string {
return fmt.Sprintf("core.Vagrantfile.registration[identifier: %s, plugin %v, set: %v, subregistrations: %s]",
r.identifier, r.plugin, r.set, r.subregistrations)
}
// Register a config component as a subconfig
func (r *registration) sub(scope string, p *plugin.Plugin) error {
if _, ok := r.subregistrations[scope]; !ok {
r.subregistrations[scope] = &subregistration{
scope: scope,
plugins: []*plugin.Plugin{p},
}
return nil
}
r.subregistrations[scope].plugins = append(
r.subregistrations[scope].plugins, p,
)
return nil
}
// Registration entry for a config component that is using within
// other config components (providers, provisioners, etc.)
type subregistration struct {
scope string // The scope is the sub-key used
plugins []*plugin.Plugin // Plugin that provides this config component
}
func (r *subregistration) String() string {
return fmt.Sprintf("core.Vagrantfile.subregistration[scope: %s, plugins: %v]", r.scope, r.plugins)
}
// Collection of config component registrations
type registrations map[string]*registration
// Initialize a new registration entry. This will create
// the entry without a plugin value set which is useful
// for adding subregistrations before toplevel registration
// has been created.
func (r registrations) init(n string) *registration {
if v, ok := r[n]; ok {
return v
}
r[n] = &registration{
identifier: n,
subregistrations: map[string]*subregistration{},
}
return r[n]
}
// Register a config component
func (r registrations) register(n string, p *plugin.Plugin) error {
if _, ok := r[n]; ok {
return fmt.Errorf("namespace %s is already registered by plugin %s", n, p.Name)
}
r[n] = &registration{
identifier: n,
plugin: p,
subregistrations: map[string]*subregistration{},
}
return nil
}
// Represents an individual Vagrantfile source
type source struct {
base *vagrant_server.Vagrantfile
finalized *component.ConfigData
unfinalized *component.ConfigData
}
// And here's our Vagrantfile!
type Vagrantfile struct {
cache cacher.Cache // Cached used for storing target configs
cleanup cleanup.Cleanup // Cleanup tasks to run on close
boxes *BoxCollection // Box collection to utilize
logger hclog.Logger // Logger
mappers []*argmapper.Func // Mappers
factory *Factory // Factory for target generation
registrations registrations // Config plugin registrations
root *component.ConfigData // Combined Vagrantfile config
rubyClient *serverclient.RubyVagrantClient // Client for the Ruby runtime
sources map[LoadLocation]*source // Vagrantfile sources
targetSource *vagrant_plugin_sdk.Ref_Project
internal interface{} // Internal instance used for running maps
m sync.Mutex
}
func (v *Vagrantfile) String() string {
return fmt.Sprintf("core.Vagrantfile[factory: %v, registrations: %s, sources: %v]",
v.factory, v.registrations, v.sources)
}
// Create a new Vagrantfile instance
func NewVagrantfile(
f *Factory,
b *BoxCollection,
m []*argmapper.Func, // Mappers to be used for type conversions
l hclog.Logger, // Logger
) *Vagrantfile {
var err error
if m == nil {
m, err = argmapper.NewFuncList(protomappers.All,
argmapper.Logger(dynamic.Logger),
)
if err == nil {
l.Error("failed to generate mapper functions",
"error", err,
)
m = []*argmapper.Func{}
}
}
v := &Vagrantfile{
cache: cacher.New(),
cleanup: cleanup.New(),
boxes: b,
logger: l.Named("vagrantfile"),
mappers: m,
factory: f,
registrations: make(registrations),
rubyClient: f.plugins.RubyClient(),
sources: make(map[LoadLocation]*source),
}
int := plugin.NewInternal(
f.plugins.LegacyBroker(),
v.cache,
v.cleanup,
v.logger,
v.mappers,
)
v.internal = int
return v
}
// Get the source Vagrantfile proto for the configured location
func (v *Vagrantfile) GetSource(
l LoadLocation, // Load location of the source
) (*vagrant_server.Vagrantfile, error) {
s, ok := v.sources[l]
if !ok {
return nil, fmt.Errorf(
"no vagrantfile source for given location (%s)",
l.String(),
)
}
return s.base, nil
}
// Register a task to be performed on close
func (v *Vagrantfile) Closer(
fn cleanup.CleanupFn, // cleanup task to perform
) {
v.cleanup.Do(fn)
}
// Perform any registered closer tasks
func (v *Vagrantfile) Close() error {
v.logger.Trace("closing vagrantfile")
return v.cleanup.Close()
}
// Add a source Vagrantfile
func (v *Vagrantfile) Source(
vf *vagrant_server.Vagrantfile, // vagrantfile source
l LoadLocation, // location of the vagrantfile source
) error {
v.m.Lock()
defer v.m.Unlock()
// If the configuration we are given is nil, ignore it
if vf == nil {
v.logger.Debug("vagrantfile is unset, not adding",
"location", l.String(),
)
return nil
}
s, err := v.newSource(vf)
if err != nil {
v.logger.Debug("failed to generate new source",
"location", l.String(),
"error", err,
)
return err
}
v.sources[l] = s
v.logger.Info("added new source to vagrantfile",
"location", l.String(),
)
return nil
}
// Register configuration plugin
func (v *Vagrantfile) Register(
info *component.ConfigRegistration, // plugin registration information
p *plugin.Plugin, // plugin to register
) (err error) {
v.m.Lock()
defer v.m.Unlock()
if info.Scope == "" {
if v.registrations == nil {
return fmt.Errorf("registrations are nil")
}
return v.registrations.register(info.Identifier, p)
}
r := v.registrations.init(info.Identifier)
return r.sub(info.Scope, p)
}
// Initialize the Vagrantfile for use. This should be called
// after inital sources are added to populate the `root` value
// with the base merged and finalized configuration.
func (v *Vagrantfile) Init() (err error) {
v.m.Lock()
defer v.m.Unlock()
v.logger.Debug("starting vagrantfile initialization",
"sources", v.sources,
)
locations := []LoadLocation{}
// Collect all the viable locations for the initial load
for i := VAGRANTFILE_BOX; i <= VAGRANTFILE_PROVIDER; i++ {
if _, ok := v.sources[i]; ok {
locations = append(locations, i)
}
}
// If our final location is finalized, and is a valid root location,
// then we use that finalized value and return. What this effectively
// allows is reusing a serialized Vagrantfile during a single run. Since
// the Vagrantfile will be parsed during the init job, when the command
// job runs, we won't need to redo the work.
var s *source
finalIdx := len(locations) - 1
if finalIdx >= 0 {
final := locations[finalIdx]
if _, ok := ValidRootLocations[final]; ok {
s = v.sources[final]
if s.finalized != nil {
v.logger.Info("setting vagrantfile root to finalized data and exiting",
"data", hclog.Fmt("%#v", s.finalized),
)
v.root = s.finalized
return
}
}
}
// Generate merged configuration data from locations
// which are currently available
var c *component.ConfigData
if c, err = v.generate(locations...); err != nil {
v.logger.Error("failed to generate initial vagrantfile configuration",
"error", err,
)
return
}
// Finalize the generated config
if v.root, err = v.finalize(c); err != nil {
v.logger.Error("failed to finalize initial vagrantfile configuration",
"error", err,
)
return
}
// Store the finalized configuration in the final source
if s != nil {
v.logger.Info("setting finalized into last vagrant source", "source", s)
if err = v.setFinalized(s, v.root); err != nil {
return
}
}
v.logger.Debug("vagrantfile initialization complete")
return
}
// Get the configuration for the given namespace
func (v *Vagrantfile) GetConfig(
namespace string, // top level key in vagrantfile
) (*component.ConfigData, error) {
raw, ok := v.root.Data[namespace]
if !ok {
v.logger.Trace("requested namespace does not exist",
"namespace", namespace,
)
return nil, fmt.Errorf("no config defined for requested namespace (%s)", namespace)
}
c, ok := raw.(*component.ConfigData)
if !ok {
v.logger.Trace("requested namespace could not be cast to config data",
"type", hclog.Fmt("%T", raw),
)
return nil, fmt.Errorf("invalid data type for requested namespace (%s)", namespace)
}
return c, nil
}
// Get the primary target name
// TODO(spox): VM options support is not implemented yet, so this
// will not return the correct value when default option
// has been specified in the Vagrantfile
func (v *Vagrantfile) PrimaryTargetName() (n string, err error) {
list, err := v.TargetNames()
if err != nil {
return
}
return list[0], nil
}
// Get list of target names defined within the Vagrantfile
func (v *Vagrantfile) TargetNames() (list []string, err error) {
list = []string{}
vm := v.getNamespace("vm")
if vm == nil {
v.logger.Trace("failed to get vm namespace from config")
return
}
dvms, ok := vm["__defined_vm_keys"]
if !ok {
keys := []string{}
for k, _ := range vm {
keys = append(keys, k)
}
v.logger.Trace("failed to locate __defined_vm_keys in vm config",
"keys", keys,
)
return
}
vmsList, ok := dvms.([]interface{})
if !ok {
v.logger.Trace("defined vm list is not a valid array type")
return
}
list = make([]string, 0, len(vmsList))
for _, val := range vmsList {
if sym, ok := val.(types.Symbol); ok {
list = append(list, string(sym))
} else {
v.logger.Trace("vm value is invalid type",
"value", val,
"type", hclog.Fmt("%T", val),
)
}
}
if len(list) == 0 {
list = append(list, "default")
}
v.logger.Trace("full list of target names found",
"targets", list,
)
return
}
// Load a new target instance
// TODO(spox): Probably add a name check against TargetNames
// before doing the config load
func (v *Vagrantfile) Target(
name, // Name of the target
provider string, // Provider backing the target
) (target core.Target, err error) {
v.logger.Info("doing lookup for target", "name", name)
name, err = v.targetNameLookup(name)
if err != nil {
return nil, err
}
conf, err := v.TargetConfig(name, provider, false)
if err != nil {
return
}
opts := []TargetOption{
WithTargetRef(
&vagrant_plugin_sdk.Ref_Target{
Name: name,
Project: v.targetSource,
},
),
}
var vf *Vagrantfile
if conf != nil {
// Convert to actual Vagrantfile for target setup
vf = conf.(*Vagrantfile)
opts = append(opts, WithTargetVagrantfile(vf))
}
target, err = v.factory.NewTarget(opts...)
if err != nil {
return nil, err
}
// Since the target config gives us a Vagrantfile which is
// attached to the project, we need to clone it and attach
// it to the target we loaded
if vf != nil {
rawTarget := target.(*Target)
tvf := vf.clone(name)
if err = tvf.Init(); err != nil {
return nil, err
}
tvf.logger = rawTarget.logger.Named("vagrantfile")
rawTarget.vagrantfile = tvf
if err = vf.Close(); err != nil {
return nil, err
}
}
return
}
// Generate a new Vagrantfile for the given target
// NOTE: This function may return a nil result without an error
// TODO(spox): Provider validation is not currently implemented
// TODO(spox): Needs box configuration applied
func (v *Vagrantfile) TargetConfig(
name, // name of the target
provider string, // provider backing the target
validateProvider bool, // validate the requested provider is supported
) (tv core.Vagrantfile, err error) {
v.m.Lock()
defer v.m.Unlock()
name, err = v.targetNameLookup(name)
if err != nil {
return nil, err
}
cid := name + "+" + provider
if cv := v.cache.Get(cid); cv != nil {
return cv.(core.Vagrantfile), nil
}
subvm, err := v.GetValue("vm", "__defined_vms", name)
if err != nil {
// If we failed to get the subvm value, then we want to
// just load the target directly so it can generate
v.logger.Warn("failed to get subvm",
"name", name,
"error", err,
)
t, err := v.factory.NewTarget(WithTargetName(name))
if err != nil {
if status.Code(err) != codes.NotFound {
return nil, err
}
t, err = v.factory.NewTarget(
WithTargetRef(
&vagrant_plugin_sdk.Ref_Target{
ResourceId: name,
},
),
)
if err != nil {
return nil, err
}
}
return t.vagrantfile, nil
}
if subvm == nil {
v.logger.Error("failed to get subvm value",
"name", name,
)
return nil, fmt.Errorf("empty value found for requested target")
}
v.logger.Info("running to proto on subvm value", "subvm", subvm)
subvmProto, err := v.toProto(subvm)
if err != nil {
return nil, err
}
v.logger.Info("sending subvm to ruby for parsing", "subvm", subvmProto)
resp, err := v.rubyClient.ParseVagrantfileSubvm(
subvmProto.(*vagrant_plugin_sdk.Config_RawRubyValue),
)
if err != nil {
v.logger.Error("failed to process target configuration",
"response", resp,
"error", err,
)
return nil, err
}
v.logger.Info("subvm configuration generated for target",
"target", name,
"config", resp,
)
newV := v.clone(name)
err = newV.Source(
&vagrant_server.Vagrantfile{
Unfinalized: resp,
},
VAGRANTFILE_TARGET,
)
if err != nil {
return nil, fmt.Errorf("failed to add target config source: %w", err)
}
if provider != "" {
resp, err = v.rubyClient.ParseVagrantfileProvider(provider,
subvmProto.(*vagrant_plugin_sdk.Config_RawRubyValue),
)
if err != nil {
return nil, err
}
err = newV.Source(
&vagrant_server.Vagrantfile{
Unfinalized: resp,
},
VAGRANTFILE_PROVIDER,
)
if err != nil {
return nil, fmt.Errorf("failed to add provider config source: %w", err)
}
}
if err = newV.Init(); err != nil {
return nil, fmt.Errorf("failed to init target config vagrantfile: %w", err)
}
vmRaw, ok := newV.root.Data["vm"]
if !ok {
return nil, fmt.Errorf("failed to get vm for delete modification")
}
vm, ok := vmRaw.(*component.ConfigData)
if !ok {
return nil, fmt.Errorf("failed to cast vm to expected map type (%T)", vmRaw)
}
delete(vm.Data, "__defined_vms")
v.cache.Register(cid, newV)
return newV, nil
}
func (v *Vagrantfile) DeleteValue(
path ...string,
) error {
if len(path) < 2 {
return fmt.Errorf("cannot delete namespace")
}
toDelete := path[len(path)-1]
path = path[0 : len(path)-1]
val, err := v.GetValue(path...)
if err != nil {
return err
}
switch m := val.(type) {
case map[interface{}]interface{}:
delete(m, toDelete)
case map[string]interface{}:
delete(m, toDelete)
default:
return fmt.Errorf("cannot delete value, invalid container type (%T)", val)
}
return nil
}
// Attempts to extract configuration information
// located at the given path
func (v *Vagrantfile) GetValue(
path ...string, // path to configuration value
) (interface{}, error) {
if len(path) == 0 {
return nil, fmt.Errorf("no lookup path provided")
}
var ok bool
var result interface{}
// Error will always be a failed path lookup so populate it now
err := fmt.Errorf("failed to locate value at given path (%#v)", path)
// First item in the path is going to be our namespace
// in the root configuration
result = v.getNamespace(path[0])
if result == nil {
v.logger.Warn("failed to get namespace for value fetch",
"namespace", path[0],
)
return nil, err
}
// Since we already used out first path value above
// be sure we start our loop from 1
for i := 1; i < len(path); i++ {
switch m := result.(type) {
case map[string]interface{}:
if result, ok = m[path[i]]; ok {
continue
}
v.logger.Warn("get value lookup failed",
"keys", path,
"current-key", path[i],
"type", "map[string]interface{}",
)
return nil, err
case map[interface{}]interface{}:
found := false
for key, val := range m {
if strKey, ok := key.(string); ok && strKey == path[i] {
found = true
result = val
break
}
if symKey, ok := key.(types.Symbol); ok && string(symKey) == path[i] {
found = true
result = val
break
}
}
if found {
continue
}
v.logger.Warn("get value lookup failed",
"keys", path,
"current-key", path[i],
"type", "map[interface{}]interface{}",
)
return nil, err
case *component.ConfigData:
if result, ok = m.Data[path[i]]; ok {
continue
}
v.logger.Warn("get value lookup failed",
"keys", path,
"current-key", path[i],
"type", "ConfigData",
)
return nil, err
case *types.RawRubyValue:
if result, ok = m.Data[path[i]]; ok {
continue
}
v.logger.Warn("get value lookup failed",
"keys", path,
"current-key", path[i],
"type", "RawRubyValue",
)
return nil, err
default:
v.logger.Warn("get value lookup failed",
"keys", path,
"current-key", path[i],
"type", "no-match",
)
return nil, err
}
}
return result, nil
}
// Returns the configuration of a specific namespace
// in the root configuration.
func (v *Vagrantfile) getNamespace(
n string, // namespace
) map[string]interface{} {
v.logger.Trace("getting requested namespace",
"namespace", n,
"self", v,
)
raw, ok := v.root.Data[n]
if !ok {
v.logger.Trace("requested namespace does not exist",
"namespace", n,
)
return nil
}
c, ok := raw.(*component.ConfigData)
if !ok {
v.logger.Trace("requested namespace could not be cast to config data",
"type", hclog.Fmt("%T", raw),
)
return nil
}
v.logger.Trace("returning data for requested namespace",
"namespace", n,
"type", hclog.Fmt("%T", c.Data),
)
return c.Data
}
// Converts the current root value into proto for storing in the origin
func (v *Vagrantfile) rootToStore() (*vagrant_plugin_sdk.Args_ConfigData, error) {
raw, err := dynamic.Map(
v.root,
(**vagrant_plugin_sdk.Args_ConfigData)(nil),
argmapper.ConverterFunc(v.mappers...),
argmapper.Typed(
context.Background(),
v.logger,
plugin.Internal(v.logger, v.mappers),
),
)
if err != nil {
return nil, err
}
return raw.(*vagrant_plugin_sdk.Args_ConfigData), nil
}
// Create a new source instance from a given Vagrantfile.
// This will handle preloading any data which is available.
func (v *Vagrantfile) newSource(
f *vagrant_server.Vagrantfile, // backing Vagrantfile proto for source
) (s *source, err error) {
s = &source{
base: f,
}
// First we need to unpack the unfinalized data.
if s.unfinalized, err = v.generateConfig(f.Unfinalized); err != nil {
return
}
// Next, if we have finalized data already set, just restore it
// and be done.
if f.Finalized != nil {
s.finalized, err = v.generateConfig(f.Finalized)
return
}
return s, nil
}
// Finalize all configuration held within the provided
// config data. This assumes the given config data is
// the top level, with each key being the namespace
// for a given config plugin
func (v *Vagrantfile) finalize(
conf *component.ConfigData, // root configuration data
) (result *component.ConfigData, err error) {
result = &component.ConfigData{
Data: make(map[string]interface{}),
}
var c core.Config
var r *component.ConfigData
for k, val := range conf.Data {
v.logger.Trace("starting configuration finalization",
"namespace", k,
)
if c, err = v.componentForKey(k); err != nil {
return
}
data, ok := val.(*component.ConfigData)
if !ok {
v.logger.Error("invalid config type",
"key", k,
"type", hclog.Fmt("%T", val),
"value", hclog.Fmt("%#v", val),
)
return nil, fmt.Errorf("config for %s is invalid type %T", k, val)
}
v.logger.Trace("finalizing configuration data",
"namespace", k,
"data", data,
)
if r, err = c.Finalize(data); err != nil {
return
}
v.logger.Trace("finalized configuration data",
"namespace", k,
)
result.Data[k] = r
v.logger.Trace("finalized data has been stored in result",
"namespace", k,
)
}
// Now we need to run through all our registered config components
// and for any we don't have a value for, and automatically finalize
for n, reg := range v.registrations {
// If no plugin is attached, skip
if reg.plugin == nil {
continue
}
// If we have data already, skip
if _, ok := result.Data[n]; ok {
continue
}
// Get the config component and send an empty request
// so we can store the default finalized config
if c, err = v.componentForKey(n); err != nil {
return
}
if r, err = c.Finalize(&component.ConfigData{}); err != nil {
return
}
result.Data[n] = r
}
v.logger.Trace("configuration data finalization is now complete")
return
}
// Set the finalized value for the given source. This
// will convert the finalized data to proto and update
// the backing Vagrantfile proto.
func (v *Vagrantfile) setFinalized(
s *source, // source to update
f *component.ConfigData, // finalized data
) error {
s.finalized = f
raw, err := dynamic.Map(
s.finalized.Data,
(**vagrant_plugin_sdk.Args_Hash)(nil),
argmapper.ConverterFunc(v.mappers...),
argmapper.Typed(
context.Background(),
v.logger,
plugin.Internal(v.logger, v.mappers),
),
)
if err != nil {
return err
}
s.base.Finalized = raw.(*vagrant_plugin_sdk.Args_Hash)
return nil
}
// Generate a config data instance by merging all unfinalized
// data from each source that is requested. The result will
// be the unfinalized result of all merged configuration.
func (v *Vagrantfile) generate(
locs ...LoadLocation, // order of sources to load
) (c *component.ConfigData, err error) {
if len(locs) == 0 {
return &component.ConfigData{Data: map[string]interface{}{}}, nil
}
c = v.sources[locs[0]].unfinalized
for idx := 1; idx < len(locs); idx++ {
i := locs[idx]
v.logger.Trace("starting vagrantfile merge",
"location", i.String(),
)
s, ok := v.sources[i]
if !ok {
v.logger.Warn("no vagrantfile set for location",
"location", i.String(),
)
continue
}
if c == nil {
v.logger.Trace("config unset, using location as base",
"location", i.String(),
)
c = s.unfinalized
continue
}
if c, err = v.merge(c, s.unfinalized); err != nil {
v.logger.Trace("failed to merge vagrantfile",
"location", i.String(),
"error", err,
)
return
}
v.logger.Trace("completed vagrantfile merge",
"location", i.String(),
)
}
return
}
// Convert a proto hash into config data
func (v *Vagrantfile) generateConfig(
value *vagrant_plugin_sdk.Args_Hash,
) (*component.ConfigData, error) {
raw, err := dynamic.Map(
&vagrant_plugin_sdk.Args_ConfigData{Data: value},
(**component.ConfigData)(nil),
argmapper.ConverterFunc(v.mappers...),
argmapper.Typed(
context.Background(),
v.logger,
v.internal,
),
)
if err != nil {
return nil, err
}
return raw.(*component.ConfigData), nil
}
// Get the configuration component for the given namespace
func (v *Vagrantfile) componentForKey(
namespace string, // namespace config component is registered under
) (core.Config, error) {
reg := v.registrations[namespace]
if reg == nil || reg.plugin == nil {
return nil, fmt.Errorf("no plugin set for config namespace '%s'", namespace)
}
c, err := reg.plugin.Component(component.ConfigType)
if err != nil {
return nil, err
}
return c.(core.Config), nil
}
// Merge two config data instances
func (v *Vagrantfile) merge(
base, // initial config data
toMerge *component.ConfigData, // config data to merge into base
) (*component.ConfigData, error) {
result := &component.ConfigData{
Data: make(map[string]interface{}),
}
// Collect all the keys we have available
keys := map[string]struct{}{}
for k, _ := range base.Data {
keys[k] = struct{}{}
}
for k, _ := range toMerge.Data {
keys[k] = struct{}{}
}
for k, _ := range keys {
c, err := v.componentForKey(k)
if err != nil {
return nil, err
}
rawBase, ok1 := base.Data[k]
rawToMerge, ok2 := toMerge.Data[k]
if ok1 && !ok2 {
result.Data[k] = rawBase
v.logger.Debug("only base value available, no merge performed",
"namespace", k,
)
continue
}
if !ok1 && ok2 {
result.Data[k] = rawToMerge
v.logger.Debug("only toMerge value available, no merge performed",
"namespace", k,
)
continue
}
var ok bool
var valBase, valToMerge *component.ConfigData
valBase, ok = rawBase.(*component.ConfigData)
if !ok {
return nil, fmt.Errorf("bad value type for merge %T", rawBase)
}
valToMerge, ok = rawToMerge.(*component.ConfigData)
if !ok {
return nil, fmt.Errorf("bad value type for merge %T", rawToMerge)
}
v.logger.Debug("merging values",
"namespace", k,
"base", valBase,
"overlay", valToMerge,
)
r, err := c.Merge(valBase, valToMerge)
if err != nil {
return nil, err
}
result.Data[k] = r
}
return result, nil
}
// Create a clone of the current Vagrantfile
func (v *Vagrantfile) clone(name string) *Vagrantfile {
reg := make(registrations, len(v.registrations))
for k, v := range v.registrations {
reg[k] = v
}
srcs := make(map[LoadLocation]*source, len(v.sources))
for k, v := range v.sources {
srcs[k] = v
}
newV := &Vagrantfile{
boxes: v.boxes,
cache: v.cache,
cleanup: cleanup.New(),
factory: v.factory,
internal: v.internal,
logger: v.logger.Named(name),
mappers: v.mappers,
registrations: reg,
rubyClient: v.rubyClient,
sources: srcs,
}
v.Closer(func() error { return newV.Close() })
int := plugin.NewInternal(
newV.factory.plugins.LegacyBroker(),
newV.factory.cache,
newV.cleanup,
newV.logger,
newV.mappers,
)
v.internal = int
return newV
}
// Convert value to proto
func (v *Vagrantfile) toProto(
value interface{},
) (proto.Message, error) {
raw, err := dynamic.Map(
value,
(*proto.Message)(nil),
argmapper.ConverterFunc(v.mappers...),
argmapper.Typed(
context.Background(),
v.logger,
v.internal,
),
)
if err != nil {
return nil, err
}
return raw.(proto.Message), nil
}
// Lookup target by name or resource id and return
// the target's name.
func (v *Vagrantfile) targetNameLookup(
nameOrId string, // target name or resource id
) (string, error) {
if cname, ok := v.cache.Fetch("lookup" + nameOrId); ok {
return cname.(string), nil
}
// Run a lookup first to verify if this target actually exists. If it does,
// then request it.
resp, err := v.factory.client.FindTarget(v.factory.ctx,
&vagrant_server.FindTargetRequest{
Target: &vagrant_server.Target{
Name: nameOrId,
ResourceId: nameOrId,
Project: v.targetSource,
},
},
)
if err != nil {
// When we are in Basis-only mode (VAGRANT_CWD does not have a
// Vagrantfile), legacy Vagrant still expects to be able to retrieve config
// for the default vm in order to successfully bootstrap its
// Vagrant::Environment. In order to retain that behavior, we allow the
// DEFAULT_VM_NAME to pass through successfully even when no targets
// exist. Note we are specifically skipping the cache registration
// below for this short circuit - we only want to do that when a target
// exists.
if s := status.Convert(err); s.Code() == codes.NotFound && nameOrId == DEFAULT_VM_NAME {
v.logger.Info("ignoring target not found error for DEFAULT_VM_NAME")
return DEFAULT_VM_NAME, nil
}
return "", err
}
// Register lookups in the local cache
v.cache.Register(
fmt.Sprintf("lookup+%s", resp.Target.Name),
resp.Target.Name,
)
v.cache.Register(
fmt.Sprintf("lookup+%s", resp.Target.ResourceId),
resp.Target.Name,
)
return resp.Target.Name, nil
}
func (v *Vagrantfile) loadToRoot(
value *vagrant_plugin_sdk.Args_ConfigData,
) error {
raw, err := dynamic.Map(
value,
(**component.ConfigData)(nil),
argmapper.ConverterFunc(v.mappers...),
argmapper.Typed(
context.Background(),
v.logger,
v.internal,
),
)
if err != nil {
return err
}
v.root = raw.(*component.ConfigData)
return nil
}
// 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.Vagrantfile = (*Vagrantfile)(nil)