vaguerent/internal/core/vagrantfile.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

1575 lines
40 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package core
import (
"context"
"fmt"
"path/filepath"
"reflect"
"strings"
"sync"
"github.com/fatih/structs"
"github.com/fatih/structtag"
"github.com/hashicorp/go-argmapper"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hclsimple"
"github.com/mitchellh/protostructure"
"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/config"
"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/localizer"
"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)
}
func (r *registration) hasSubs() bool {
return len(r.subregistrations) > 0
}
// 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 // proto representing the Vagrantfile
finalized *component.ConfigData // raw configuration data after finalization
unfinalized *component.ConfigData // raw configuration data prior to finalization
vagrantfile *config.Vagrantfile // vagrantfile as processed
}
// 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)
}
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 configuration for the given namespace and load
// it into the provided configuration struct
func (v *Vagrantfile) Get(
namespace string, // top level key in vagrantfile
config any, // pointer to configuration struct to populate
) error {
return 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, true)
if err != nil {
return
}
opts := []TargetOption{
WithTargetRef(
&vagrant_plugin_sdk.Ref_Target{
Name: name,
Project: v.targetSource,
},
),
WithProvider(provider),
}
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
}
rawTarget := target.(*Target)
if provider != "" {
rawTarget.target.Provider = provider
}
// 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 {
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): 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
}
if provider != "" {
pp, err := v.factory.plugins.Find(provider, component.ProviderType)
if err != nil {
return nil, err
}
if validateProvider {
usable, err := pp.Component.(core.Provider).Usable()
if !usable {
if errStatus, ok := status.FromError(err); ok {
return nil, localizer.LocalizeStatusErr(
"provider_not_usable",
map[string]string{"Provider": provider, "Machine": name},
errStatus,
true,
)
}
}
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 target",
"name", name,
"error", err,
)
t, err := v.factory.NewTarget(
WithTargetName(name),
WithTargetProjectRef(v.targetSource),
)
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,
unfinalized: &component.ConfigData{},
}
// Start with loading the Vagrantfile if it hasn't
// yet been loaded
if s.base.Unfinalized == nil {
if err := v.loadVagrantfile(s); err != nil {
return nil, err
}
}
if s.unfinalized == nil {
// 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
}
func (v *Vagrantfile) loadVagrantfile(
source *source,
) error {
var err error
if source.base.Path == nil {
return nil
}
// Set the format and load the file based on extension
switch filepath.Ext(source.base.Path.Path) {
case ".hcl":
source.base.Format = vagrant_server.Vagrantfile_HCL
if err = v.parseHCL(source); err != nil {
return err
}
// Need to load and encode from here. Thoughts about protostructure and
// if we should be treating hcl processed config differently
case ".json":
source.base.Format = vagrant_server.Vagrantfile_JSON
if err = v.parseHCL(source); err != nil {
return err
}
default:
source.base.Format = vagrant_server.Vagrantfile_RUBY
source.base.Unfinalized, err = v.rubyClient.ParseVagrantfile(source.base.Path.Path)
source.unfinalized, err = v.generateConfig(source.base.Unfinalized)
if err != nil {
return err
}
}
return nil
}
func (v *Vagrantfile) parseHCL(source *source) error {
if source == nil {
panic("vagrantfile source is nil")
}
target := &config.Vagrantfile{}
// This handles loading native configuration
err := hclsimple.DecodeFile(source.base.Path.Path, nil, target)
if err != nil {
v.logger.Error("failed to decode vagrantfile", "path", source.base.Path.Path, "error", err)
return err
}
if source.unfinalized == nil {
source.unfinalized = &component.ConfigData{}
}
source.unfinalized.Source = structs.Name(target)
source.unfinalized.Data = map[string]interface{}{}
// Serialize all non-plugin configuration values/namespaces
fields := structs.Fields(target)
for _, field := range fields {
// TODO(spox): This is just implemented enough to get things
// working. It needs to be reimplemented with helpers and all
// that stuff.
// if the field is the body content or remaining content after
// being parsed, ignore it
tags, err := structtag.Parse(`hcl:"` + field.Tag("hcl") + `"`)
if err != nil {
return err
}
tag, err := tags.Get("hcl")
if err != nil {
return err
}
skip := false
for _, opt := range tag.Options {
if opt == "remain" || opt == "body" {
skip = true
}
}
if skip {
continue
}
// If there is no field name, or if the value is empty
// just ignore it
n := field.Name()
np := strings.Split(field.Tag("hcl"), ",")
if len(np) > 0 {
n = np[0]
}
if n == "" {
continue
}
val := field.Value()
if field.IsZero() {
val = &component.ConfigData{Data: map[string]any{}}
}
rval := reflect.ValueOf(val)
// If the value is a struct, convert it to a map before storing
if rval.Kind() == reflect.Struct || (rval.Kind() == reflect.Pointer && rval.Elem().Kind() == reflect.Struct) {
val = &component.ConfigData{
Source: structs.Name(val),
Data: structs.Map(val),
}
}
source.unfinalized.Data[n] = val
}
// Now load all configuration which has registrations
for namespace, registration := range v.registrations {
// If no plugin is attached to registration, skip
if registration.plugin == nil {
continue
}
// Not handling subregistrations yet
if registration.hasSubs() {
continue
}
config, err := v.componentForKey(namespace)
if err != nil {
v.logger.Error("failed to dispense config plugin", "namespace", namespace, "error", err)
return err
}
configTarget, err := config.Struct()
if err != nil {
v.logger.Error("failed to receive config structure", "namespace", namespace)
return err
}
// If the struct value is a boolean, then the plugin is Ruby based. Take any configuration
// that is currently defined (would have been populated via HCL file) and ship it to the
// plugin to attempt to initialize itself with the existing data.
//
// TODO(spox): the data structure generated should use the hcl tag name as the key which
// should match up better with the expected ruby configs
if _, ok := configTarget.(bool); ok {
vc := structs.New(target)
// Find the configuration data for the current namespace
var field *structs.Field
for _, f := range vc.Fields() {
t := f.Tag("hcl")
np := strings.Split(t, ",")
if len(np) < 1 {
break
}
n := np[0]
if n == namespace {
field = f
break
}
}
var data map[string]any
// If no field was found for namespace, or the value is empty, skip
if field == nil || field.IsZero() {
data = map[string]any{}
} else {
data = structs.Map(field.Value())
}
cd, err := config.Init(&component.ConfigData{Data: data})
if err != nil {
v.logger.Error("ruby config init failed", "error", err)
return err
}
source.unfinalized.Data[namespace] = cd
v.logger.Trace("stored unfinalized configuration data", "namespace", namespace, "value", cd)
continue
}
v.logger.Trace("config structure received", "namespace", namespace, "struct", hclog.Fmt("%#v", configTarget))
// A protostructure.Struct value is expected to build the configuration structure
cs, ok := configTarget.(*protostructure.Struct)
if !ok {
v.logger.Error("config value is not protostructure.Struct", "namespace", namespace,
"type", hclog.Fmt("%T", configTarget))
return fmt.Errorf("invalid configuration type received %T", configTarget)
}
// Create the configuration structure
configStruct, err := protostructure.New(cs)
if err != nil {
return err
}
// Build a struct which contains the configuration structure
// so the Vagrantfile contents can be decoded into it
wrapStructType := reflect.StructOf(
[]reflect.StructField{
{
Name: "Remain",
Type: reflect.TypeOf((*hcl.Body)(nil)).Elem(),
Tag: reflect.StructTag(`hcl:",remain"`),
},
{
Name: "Content",
Type: reflect.TypeOf(configStruct),
Tag: reflect.StructTag(fmt.Sprintf(`hcl:"%s,block"`, namespace)),
},
},
)
wrapStruct := reflect.New(wrapStructType)
// Decode the Vagrantfile into the new wrapper
diag := gohcl.DecodeBody(target.Remain, nil, wrapStruct.Interface())
if diag.HasErrors() {
return fmt.Errorf("failed to load config namespace %s: %s", namespace, diag.Error())
}
// Extract the decoded configuration
str := structs.New(wrapStruct.Interface())
f, ok := str.FieldOk("Content")
if !ok {
v.logger.Debug("missing 'Content' field in wrapper struct", "wrapper", wrapStruct.Interface())
return fmt.Errorf("unexpected missing data during Vagrantfile decoding")
}
decodedConfig := f.Value()
v.logger.Trace("configuration value decoded", "namespace", namespace, "config", decodedConfig)
// Use reflect to do a proper nil check since interface
// will be typed
if reflect.ValueOf(decodedConfig).IsNil() {
v.logger.Debug("decoded configuration was nil, continuing...", "namespace", namespace)
continue
}
// Convert the configuration value into a map and store in the unfinalized
// data for the plugin's namespace
decodedMap := structs.Map(decodedConfig)
source.unfinalized.Data[namespace] = &component.ConfigData{Data: decodedMap}
v.logger.Trace("stored unfinalized configuration data", "namespace", namespace, "value", decodedMap)
}
return 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
seen := map[string]struct{}{}
for n, val := range conf.Data {
seen[n] = struct{}{}
reg, ok := v.registrations[n]
if !ok {
v.logger.Warn("no plugin registered", "namespace", n)
continue
}
v.logger.Trace("starting finalization", "namespace", n)
// if no plugin attached, skip
if reg.plugin == nil {
v.logger.Warn("missing plugin registration", "namespace", n)
continue
}
var data *component.ConfigData
data, ok = val.(*component.ConfigData)
if !ok {
v.logger.Error("invalid data for namespace", "type", hclog.Fmt("%T", val))
return nil, fmt.Errorf("invalid data encountered in '%s' namespace", n)
}
v.logger.Trace("finalizing", "namespace", n, "data", data)
if c, err = v.componentForKey(n); err != nil {
return nil, err
}
if r, err = c.Finalize(data); err != nil {
return nil, err
}
v.logger.Trace("finalized", "namespace", n, "data", r)
result.Data[n] = r
}
for n, reg := range v.registrations {
if _, ok := result.Data[n]; ok {
continue
}
if reg.plugin == nil {
v.logger.Warn("missing plugin registration", "namespace", n)
continue
}
if c, err = v.componentForKey(n); err != nil {
return nil, err
}
if result.Data[n], err = c.Finalize(&component.ConfigData{}); err != nil {
return nil, err
}
}
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 {
v.logger.Error("failed to convert data into finalized proto value")
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
overlay *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 overlay.Data {
keys[k] = struct{}{}
}
for k, _ := range keys {
c, err := v.componentForKey(k)
if err != nil {
v.logger.Debug("no config component for namespace, skipping", "namespace", k)
}
rawBase, okBase := base.Data[k]
rawOverlay, okOverlay := overlay.Data[k]
if okBase && !okOverlay {
result.Data[k] = rawBase
v.logger.Debug("only base value available, no merge performed",
"namespace", k, "config", rawBase,
)
continue
}
if !okBase && okOverlay {
result.Data[k] = rawOverlay
v.logger.Debug("only overlay value available, no merge performed",
"namespace", k, "config", rawOverlay,
)
continue
}
if c == nil {
result.Data[k] = rawOverlay
v.logger.Debug("no component for namespace, applying overlay directly",
"namespace", k, "config", rawOverlay,
)
continue
}
var ok bool
var valBase, valOverlay *component.ConfigData
valBase, ok = rawBase.(*component.ConfigData)
if !ok {
return nil, fmt.Errorf("bad value type for merge %T", rawBase)
}
valOverlay, ok = rawOverlay.(*component.ConfigData)
if !ok {
return nil, fmt.Errorf("bad value type for merge %T", rawOverlay)
}
v.logger.Debug("merging values",
"namespace", k,
"base", valBase,
"overlay", valOverlay,
)
r, err := c.Merge(valBase, valOverlay)
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,
targetSource: v.targetSource,
}
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
}
func structToMap(in any) map[string]any {
new := structs.Map(in)
for key, _ := range new {
val := new[key]
rval := reflect.ValueOf(val)
if rval.Kind() == reflect.Struct || (rval.Kind() == reflect.Pointer && rval.Elem().Kind() == reflect.Struct) {
new[key] = structToMap(val)
}
}
return new
}
var _ core.Vagrantfile = (*Vagrantfile)(nil)