Reorganize vagrantfile implementation, support provider overrides

This commit is contained in:
Chris Roberts 2022-06-20 13:02:01 -07:00
parent f50f700e5c
commit 97c51a56cd
2 changed files with 347 additions and 287 deletions

View File

@ -12,11 +12,12 @@ func _() {
_ = x[VAGRANTFILE_BASIS-1]
_ = x[VAGRANTFILE_PROJECT-2]
_ = x[VAGRANTFILE_TARGET-3]
_ = x[VAGRANTFILE_PROVIDER-4]
}
const _LoadLocation_name = "BoxBasisProjectTarget"
const _LoadLocation_name = "BoxBasisProjectTargetProvider"
var _LoadLocation_index = [...]uint8{0, 3, 8, 15, 21}
var _LoadLocation_index = [...]uint8{0, 3, 8, 15, 21, 29}
func (i LoadLocation) String() string {
if i >= LoadLocation(len(_LoadLocation_index)-1) {

View File

@ -30,6 +30,7 @@ type originScope interface {
Boxes() (core.BoxCollection, error)
Broker() *goplugin.GRPCBroker
Cache() cacher.Cache
Closer(func() error)
LoadTarget(...TargetOption) (*Target, error)
}
@ -39,19 +40,19 @@ type originScope interface {
type LoadLocation uint8
const (
VAGRANTFILE_BOX LoadLocation = iota // Box
VAGRANTFILE_BASIS // Basis
VAGRANTFILE_PROJECT // Project
VAGRANTFILE_TARGET // Target
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 = []LoadLocation{
VAGRANTFILE_BOX,
VAGRANTFILE_BASIS,
VAGRANTFILE_PROJECT,
VAGRANTFILE_TARGET,
var ValidRootLocations = map[LoadLocation]struct{}{
VAGRANTFILE_BASIS: {},
VAGRANTFILE_PROJECT: {},
VAGRANTFILE_TARGET: {},
}
// Registration entry for a config component
@ -136,6 +137,7 @@ type source struct {
// 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
logger hclog.Logger // Logger
mappers []*argmapper.Func // Mappers
@ -145,7 +147,7 @@ type Vagrantfile struct {
rubyClient *serverclient.RubyVagrantClient // Client for the Ruby runtime
sources map[LoadLocation]*source // Vagrantfile sources
internal interface{}
internal interface{} // Internal instance used for running maps
m sync.Mutex
}
@ -154,43 +156,6 @@ func (v *Vagrantfile) String() string {
v.origin, v.registrations, v.sources)
}
// Create a clone of the current Vagrantfile
func (v *Vagrantfile) clone(name string, origin originScope) *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{
cleanup: cleanup.New(),
logger: v.logger.Named(name),
mappers: v.mappers,
origin: origin,
registrations: reg,
rubyClient: v.rubyClient,
sources: srcs,
}
if origin != nil {
int := plugin.NewInternal(
origin.Broker(),
origin.Cache(),
newV.cleanup,
newV.logger,
newV.mappers,
)
newV.internal = int
} else {
newV.internal = v.internal
}
v.Closer(func() error { return newV.Close() })
return newV
}
// Create a new Vagrantfile instance
func NewVagrantfile(
o originScope,
@ -212,6 +177,7 @@ func NewVagrantfile(
}
copy(mappers[len(protomappers.All)-1:len(protomappers.All)+len(m)], m)
v := &Vagrantfile{
cache: cacher.New(),
cleanup: cleanup.New(),
logger: l.Named("vagrantfile"),
mappers: mappers,
@ -326,26 +292,32 @@ func (v *Vagrantfile) Init() (err error) {
"sources", v.sources,
)
// Collect all the viable locations for the initial load
locations := []LoadLocation{}
for _, i := range ValidRootLocations {
// 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, just load that directly
// 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]
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
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
}
}
}
@ -368,7 +340,6 @@ func (v *Vagrantfile) Init() (err error) {
}
// Store the finalized configuration in the final source
if s != nil {
if err = v.setFinalized(s, v.root); err != nil {
return
@ -380,205 +351,6 @@ func (v *Vagrantfile) Init() (err error) {
return
}
// 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,
)
}
v.logger.Trace("configuration data finalization is now complete")
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,
plugin.Internal(v.logger, v.mappers),
),
)
if err != nil {
return nil, err
}
return raw.(*component.ConfigData), 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
}
// 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,
)
r, err := c.Merge(valBase, valToMerge)
if err != nil {
return nil, err
}
result.Data[k] = r
}
return result, nil
}
// Get the configuration for the given namespace
func (v *Vagrantfile) GetConfig(
namespace string, // top level key in vagrantfile
@ -680,11 +452,29 @@ func (v *Vagrantfile) Target(
return
}
// Convert to actual Vagrantfile for target setup
vf := conf.(*Vagrantfile)
target, err = v.origin.LoadTarget(
WithTargetName(name),
WithTargetVagrantfile(conf.(*Vagrantfile)),
WithTargetVagrantfile(vf),
)
if err != nil {
return
}
// 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
rawTarget := target.(*Target)
tvf := v.clone(name, rawTarget)
rawTarget.vagrantfile = tvf
if err = vf.Close(); err != nil {
return nil, err
}
return
}
@ -698,6 +488,11 @@ func (v *Vagrantfile) TargetConfig(
v.m.Lock()
defer v.m.Unlock()
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 {
v.logger.Error("failed to get subvm",
@ -728,6 +523,11 @@ func (v *Vagrantfile) TargetConfig(
return nil, err
}
v.logger.Info("subvm configuration generated for target",
"target", name,
"config", resp,
)
newV := v.clone(name, v.origin)
err = newV.Source(
&vagrant_server.Vagrantfile{
@ -740,10 +540,30 @@ func (v *Vagrantfile) TargetConfig(
return nil, fmt.Errorf("failed to add target config source: %w", err)
}
if provider != "" {
resp, err = v.rubyClient.ParseVagrantfileProvider(provider,
subvm.(*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)
}
v.cache.Register(cid, newV)
return newV, nil
}
@ -867,33 +687,6 @@ func (v *Vagrantfile) getNamespace(
return c.Data
}
// 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
}
// 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(
@ -936,3 +729,269 @@ func (v *Vagrantfile) newSource(
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,
)
}
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,
plugin.Internal(v.logger, v.mappers),
),
)
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,
)
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, origin originScope) *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{
cache: cacher.New(),
cleanup: cleanup.New(),
logger: v.logger.Named(name),
mappers: v.mappers,
origin: origin,
registrations: reg,
rubyClient: v.rubyClient,
sources: srcs,
}
if origin != nil {
int := plugin.NewInternal(
origin.Broker(),
origin.Cache(),
newV.cleanup,
newV.logger,
newV.mappers,
)
newV.internal = int
} else {
newV.internal = v.internal
}
origin.Closer(func() error { return newV.Close() })
return newV
}
var _ core.Vagrantfile = (*Vagrantfile)(nil)