diff --git a/internal/core/loadlocation_string.go b/internal/core/loadlocation_string.go index fb840f356..b0ef70467 100644 --- a/internal/core/loadlocation_string.go +++ b/internal/core/loadlocation_string.go @@ -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) { diff --git a/internal/core/vagrantfile.go b/internal/core/vagrantfile.go index 9c84b6731..4e5c7f9f1 100644 --- a/internal/core/vagrantfile.go +++ b/internal/core/vagrantfile.go @@ -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)