Refactor how scopes are created and initialized

This commit is contained in:
Chris Roberts 2022-07-06 12:53:36 -07:00
parent b82d55cc37
commit 51d8c84740
3 changed files with 852 additions and 1005 deletions

View File

@ -12,8 +12,9 @@ import (
"github.com/hashicorp/go-argmapper"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-multierror"
goplugin "github.com/hashicorp/go-plugin"
"github.com/pkg/errors"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"github.com/hashicorp/vagrant-plugin-sdk/component"
@ -41,47 +42,40 @@ import (
// finished with the basis to properly clean
// up any open resources.
type Basis struct {
basis *vagrant_server.Basis
boxCollection *BoxCollection
cache cacher.Cache
cleaner cleanup.Cleanup
corePlugins *CoreManager
ctx context.Context
client *serverclient.VagrantClient
dir *datadir.Basis
factory *Factory
index *TargetIndex
jobInfo *component.JobInfo
logger hclog.Logger
m sync.Mutex
mappers []*argmapper.Func
plugins *plugin.Manager
projects map[string]*Project
seedValues *core.Seeds
statebag core.StateBag
ui terminal.UI
vagrantfile *Vagrantfile
}
basis *vagrant_server.Basis // stored basis data
boxCollection *BoxCollection // box collection for this basis
cache cacher.Cache // local basis cache
cleaner cleanup.Cleanup // cleanup tasks to be run on close
client *serverclient.VagrantClient // client to vagrant server
corePlugins *CoreManager // manager for the core plugin types
ctx context.Context // local context
dir *datadir.Basis // data directory for basis
factory *Factory // scope factory
index *TargetIndex // index of targets within basis
jobInfo *component.JobInfo // jobInfo is the base job info for executed functions
logger hclog.Logger // basis specific logger
mappers []*argmapper.Func // mappers for basis
plugins *plugin.Manager // basis scoped plugin manager
ready bool // flag that instance is ready
seedValues *core.Seeds // seed values to be applied when running commands
statebag core.StateBag // statebag to persist values
ui terminal.UI // basis UI (non-prefixed)
vagrantfile *Vagrantfile // vagrantfile instance for basis
// Cache implements originScope
func (b *Basis) Cache() cacher.Cache {
return b.cache
}
// Broker implements originScope
func (b *Basis) Broker() *goplugin.GRPCBroker {
return b.plugins.LegacyBroker()
m sync.Mutex
}
// NewBasis creates a new Basis with the given options.
func NewBasis(ctx context.Context, opts ...BasisOption) (b *Basis, err error) {
b = &Basis{
func NewBasis(ctx context.Context, opts ...BasisOption) (*Basis, error) {
var err error
b := &Basis{
basis: &vagrant_server.Basis{},
cache: cacher.New(),
cleaner: cleanup.New(),
ctx: ctx,
logger: hclog.L(),
mappers: []*argmapper.Func{},
jobInfo: &component.JobInfo{},
projects: map[string]*Project{},
seedValues: core.NewSeeds(),
statebag: NewStateBag(),
}
@ -93,33 +87,80 @@ func NewBasis(ctx context.Context, opts ...BasisOption) (b *Basis, err error) {
}
if err != nil {
return
return nil, err
}
if b.logger.IsTrace() {
b.logger = b.logger.Named("basis")
} else {
b.logger = b.logger.ResetNamed("vagrant.core.basis")
}
return b, nil
}
// Create the manager for handling core plugins
b.corePlugins = NewCoreManager(ctx, b.logger)
func (b *Basis) Init() error {
var err error
if b.basis == nil {
return nil, fmt.Errorf("basis data was not properly loaded")
// If ready then Init was already run
if b.ready {
return nil
}
// Client is required to be provided
if b.client == nil {
return nil, fmt.Errorf("client was not provided to basis")
return fmt.Errorf("vagrant server client was not provided to basis")
}
// If we don't have a data directory set, lets do that now
// TODO(spox): actually do that
if b.dir == nil {
return nil, fmt.Errorf("WithDataDir must be specified")
// If no plugin manager was provided, force an error
if b.plugins == nil {
return fmt.Errorf("plugin manager was not provided to basis")
}
// Update our plugin manager to be a sub manager so we close
// it early if needed
b.plugins = b.plugins.Sub("basis")
// Configure our logger
b.logger = b.logger.Named("basis")
b.logger = b.logger.With("basis", b)
// Attempt to reload the basis to populate our
// data. If the basis is not found, create it.
err = b.Reload()
if err != nil {
stat, ok := status.FromError(err)
if !ok || stat.Code() != codes.NotFound {
return err
}
// Project doesn't exist so save it to persist
if err = b.Save(); err != nil {
return err
}
}
// If the basis directory is unset, set it
if b.dir == nil {
if b.dir, err = datadir.NewBasis(b.basis.Name); err != nil {
return err
}
}
// If the mappers aren't already set, load known mappers
if len(b.mappers) == 0 {
b.mappers, err = argmapper.NewFuncList(protomappers.All,
argmapper.Logger(dynamic.Logger),
)
if err != nil {
return err
}
locals, err := argmapper.NewFuncList(Mappers, argmapper.Logger(dynamic.Logger))
if err != nil {
return err
}
b.mappers = append(b.mappers, locals...)
}
// Create the manager for handling core plugins
b.corePlugins = NewCoreManager(b.ctx, b.logger)
// Setup our index
b.index = &TargetIndex{
ctx: b.ctx,
@ -130,37 +171,86 @@ func NewBasis(ctx context.Context, opts ...BasisOption) (b *Basis, err error) {
// If no UI was provided, initialize a console UI
if b.ui == nil {
b.ui = terminal.ConsoleUI(ctx)
b.ui = terminal.ConsoleUI(b.ctx)
}
// Create our vagrantfile
b.vagrantfile = NewVagrantfile(b, b.plugins.RubyClient(), b.mappers, b.logger)
b.vagrantfile = NewVagrantfile(b.factory, b.boxCollection, b.mappers, b.logger)
// Register the basis as a Vagrantfile source
b.vagrantfile.Source(b.basis.Configuration, VAGRANTFILE_BASIS)
// If the mappers aren't already set, load known mappers
if len(b.mappers) == 0 {
b.mappers, err = argmapper.NewFuncList(protomappers.All,
argmapper.Logger(dynamic.Logger),
)
// Register configuration plugins when they are loaded
b.plugins.Initializer(b.configRegistration)
// Register any configuration plugins already loaded
cfgs, err := b.plugins.Typed(component.ConfigType)
if err != nil {
return err
}
for _, cp := range cfgs {
b.logger.Trace("registering existing config plugin",
"name", cp,
)
p, err := b.plugins.Get(cp, component.ConfigType)
if err != nil {
return
b.logger.Error("failed to get requested plugin",
"name", cp,
"error", err,
)
return err
}
if err = b.configRegistration(p, b.logger); err != nil {
return err
}
}
// Ensure any modifications to the basis are persisted
// Configure plugins with cache instance
b.plugins.Configure(b.setPluginCache)
// Configure plugins to have seeds set
b.plugins.Configure(b.setPluginSeeds)
// If we have legacy vagrant loaded, configure managers
if b.plugins.LegacyEnabled() {
// Configure plugins to have plugin manager set (used by legacy)
b.plugins.Configure(b.setPluginManager)
// Configure plugins to have a core plugin manager set (used by legacy)
b.plugins.Configure(b.setPluginCoreManager)
}
// Load any plugins that may be available
if err = b.plugins.Discover(b.dir.ConfigDir().Join("plugins")); err != nil {
b.logger.Error("basis setup failed during plugin discovery",
"directory", b.dir.ConfigDir().Join("plugins"),
"error", err,
)
return err
}
// Set seeds for any plugins that may be used
b.seed(nil)
// Initialize the Vagrantfile for the basis
if err = b.vagrantfile.Init(); err != nil {
b.logger.Error("basis setup failed to initialize vagrantfile",
"error", err,
)
return err
}
// Store our configuration
sv, err := b.vagrantfile.GetSource(VAGRANTFILE_BASIS)
if err != nil {
return err
}
b.basis.Configuration = sv
// Close the plugin manager
b.Closer(func() error {
// Update our configuration before we save
v, err := b.vagrantfile.GetSource(VAGRANTFILE_BASIS)
if err != nil {
b.logger.Debug("failed to retrieve vagrantfile configuration",
"reason", err,
)
} else {
b.basis.Configuration = v
}
return b.Save()
return b.plugins.Close()
})
// Close the core manager
@ -178,215 +268,31 @@ func NewBasis(ctx context.Context, opts ...BasisOption) (b *Basis, err error) {
return b.index.Close()
})
// Add in local mappers
for _, fn := range Mappers {
f, err := argmapper.NewFunc(fn,
argmapper.Logger(dynamic.Logger),
)
if err != nil {
return nil, err
}
b.mappers = append(b.mappers, f)
}
// Save ourself when closed
b.Closer(func() error {
return b.Save()
})
// If no plugin manager was provided, force an error
if b.plugins == nil {
return nil, fmt.Errorf("no plugin manager provided")
}
// Register configuration plugins when they are loaded
regFn := func(p *plugin.Plugin, l hclog.Logger) error {
if !p.HasType(component.ConfigType) {
b.logger.Warn("plugin does not implement config component type",
"name", p.Name,
)
return nil
}
b.logger.Debug("registering configuration component",
"name", p.Name,
)
i, err := p.Manager().Find(p.Name, component.ConfigType)
if err != nil {
b.logger.Error("failed to load configuration component",
"name", p.Name,
"error", err,
)
return err
}
c, ok := i.Component.(core.Config)
if !ok {
return fmt.Errorf("component instance is not valid config: %s", p.Name)
}
info, err := c.Register()
if err != nil {
b.logger.Error("failed to get registration information from plugin",
"name", p.Name,
"error", err,
)
return err
}
b.logger.Info("registering configuration component",
"plugin", p.Name,
"info", *info,
)
return b.vagrantfile.Register(info, p)
}
b.plugins.Initializer(regFn)
// Register any configuration plugins already loaded
cfgs, err := b.plugins.Typed(component.ConfigType)
if err != nil {
return nil, err
}
for _, cp := range cfgs {
b.logger.Trace("registering existing config plugin",
"name", cp,
)
p, err := b.plugins.Get(cp, component.ConfigType)
if err != nil {
b.logger.Error("failed to get requested plugin",
"name", cp,
"error", err,
)
return nil, err
}
if err = regFn(p, b.logger); err != nil {
return nil, err
}
}
// Configure plugins with cache instance
b.plugins.Configure(
func(i *plugin.Instance, l hclog.Logger) error {
if c, ok := i.Component.(interface {
SetCache(cacher.Cache)
}); ok {
b.logger.Trace("setting cache on plugin instance",
"name", i.Name,
"component", hclog.Fmt("%T", i.Component),
)
c.SetCache(b.cache)
} else {
b.logger.Warn("cannot set cache on plugin instance",
"name", i.Name,
"component", hclog.Fmt("%T", i.Component),
)
}
return nil
},
)
// Configure plugins to have seeds set
b.plugins.Configure(
func(i *plugin.Instance, l hclog.Logger) error {
if s, ok := i.Component.(core.Seeder); ok {
if err := s.Seed(b.seedValues); err != nil {
return err
}
}
return nil
},
)
// If we have legacy vagrant loaded, configure managers
if b.plugins.LegacyEnabled() {
// Configure plugins to have plugin manager set (used by legacy)
b.plugins.Configure(
func(i *plugin.Instance, l hclog.Logger) error {
s, ok := i.Component.(plugin.HasPluginMetadata)
if !ok {
l.Warn("plugin does not support metadata, cannot assign plugin manager",
"component", i.Type.String(),
"name", i.Name,
)
return nil
}
srv, err := b.plugins.Servinfo()
if err != nil {
l.Warn("failed to get plugin manager information",
"error", err,
)
return nil
}
s.SetRequestMetadata("plugin_manager", string(srv))
return nil
},
)
// Configure plugins to have a core plugin manager set (used by legacy)
b.plugins.Configure(
func(i *plugin.Instance, l hclog.Logger) error {
s, ok := i.Component.(plugin.HasPluginMetadata)
if !ok {
l.Warn("plugin does not support metadata, cannot assign plugin manager",
"component", i.Type.String(),
"name", i.Name,
)
return nil
}
srv, err := b.corePlugins.Servinfo(b.plugins.LegacyBroker())
if err != nil {
l.Warn("failed to get plugin manager information",
"error", err,
)
return nil
}
s.SetRequestMetadata("core_plugin_manager", string(srv))
return nil
},
)
}
if err = b.plugins.Discover(b.dir.ConfigDir().Join("plugins")); err != nil {
b.logger.Error("basis setup failed during plugin discovery",
"directory", b.dir.ConfigDir().Join("plugins").String(),
"error", err,
)
return
}
// Set seeds for any plugins that may be used
b.seed(nil)
// Initialize the Vagrantfile for the basis
if err = b.vagrantfile.Init(); err != nil {
b.logger.Error("basis setup failed to initialize vagrantfile",
"error", err,
)
return
}
// Mark basis as being initialized
b.ready = true
b.logger.Info("basis initialized")
return
}
func (b *Basis) LoadTarget(topts ...TargetOption) (t *Target, err error) {
return nil, fmt.Errorf("targets cannot be loaded from a basis")
return nil
}
// Provide nice output in logger
func (b *Basis) String() string {
return fmt.Sprintf("core.Basis:[name: %s resource_id: %s address: %p]",
b.basis.Name, b.basis.ResourceId, b)
}
// Config implements core.Basis
func (b *Basis) Config() (core.Vagrantfile, error) {
return b.vagrantfile, nil
}
// CWD implements core.Basis
func (p *Basis) CWD() (path path.Path, err error) {
return paths.VagrantCwd()
}
@ -654,7 +560,7 @@ func (b *Basis) Host() (host core.Host, err error) {
// Initializes the basis for running a command. This will inspect
// all registered components and extract things like custom command
// information before an actual command is run
func (b *Basis) Init() (result *vagrant_server.Job_InitResult, err error) {
func (b *Basis) RunInit() (result *vagrant_server.Job_InitResult, err error) {
b.logger.Debug("running init for basis")
result = &vagrant_server.Job_InitResult{
Commands: []*vagrant_plugin_sdk.Command_CommandInfo{},
@ -684,133 +590,6 @@ func (b *Basis) Init() (result *vagrant_server.Job_InitResult, err error) {
return
}
// Looks up a project which has already been loaded and is cached
// by the project's name or resource ID. Will return nil if the
// project is not cached.
//
// NOTE: Generally the `LoadProject` function will be preferred
// as it will return the cached value if previously loaded
// or load the project if not found.
func (b *Basis) Project(nameOrId string) *Project {
if p, ok := b.projects[nameOrId]; ok {
return p
}
for _, p := range b.projects {
if p.project.ResourceId == nameOrId {
return p
}
}
return nil
}
// Load a project within the current basis. If the project is not found, it
// will be created.
func (b *Basis) LoadProject(popts ...ProjectOption) (p *Project, err error) {
b.m.Lock()
defer b.m.Unlock()
// Create our project
p = &Project{
ctx: b.ctx,
cleanup: cleanup.New(),
basis: b,
logger: b.logger,
mappers: b.mappers,
targets: map[string]*Target{},
ui: b.ui,
}
// Apply any options provided
for _, opt := range popts {
if oerr := opt(p); oerr != nil {
err = multierror.Append(err, oerr)
}
}
if err != nil {
return
}
// If we already have this project setup, use it instead
if project := b.Project(p.project.ResourceId); project != nil {
return project, nil
}
if p.logger.IsTrace() {
p.logger = p.logger.Named("project")
} else {
p.logger = p.logger.ResetNamed("vagrant.core.project")
}
// Ensure project directory is set
if p.dir == nil {
if p.dir, err = b.dir.Project(p.project.Name); err != nil {
return
}
}
// Load any plugins that may be installed locally to the project
if err = b.plugins.Discover(path.NewPath(p.project.Path).Join(".vagrant").Join("plugins")); err != nil {
b.logger.Error("project setup failed during plugin discovery",
"directory", path.NewPath(p.project.Path).Join(".vagrant").Join("plugins").String(),
"error", err,
)
return nil, err
}
// Clone our vagrantfile to use in the new project
v := b.vagrantfile.clone("project", p)
p.Closer(func() error { return v.Close() })
// Add the project vagrantfile
err = v.Source(p.project.Configuration, VAGRANTFILE_PROJECT)
if err != nil {
return nil, err
}
// Init the vagrantfile so the config is available
if err = v.Init(); err != nil {
return nil, err
}
p.vagrantfile = v
// Ensure any modifications to the project are persisted
p.Closer(func() error {
// Save any configuration updates
v, err := p.vagrantfile.GetSource(VAGRANTFILE_PROJECT)
if err != nil {
p.logger.Debug("failed to retrieve vagrantfile",
"reason", err,
)
}
p.project.Configuration = v
return p.Save()
})
// Remove ourself from cached projects
p.Closer(func() error {
b.m.Lock()
defer b.m.Unlock()
delete(b.projects, p.project.ResourceId)
delete(b.projects, p.Name())
return nil
})
// Set seeds for any plugins that may be used
p.seed(nil)
// Initialize any targets defined in the project
if err = p.InitTargets(); err != nil {
return
}
// Set our loaded project into the basis
b.projects[p.project.ResourceId] = p
b.logger.Info("done setting up new project instance")
return
}
// Register functions to be called when closing this basis
func (b *Basis) Closer(c func() error) {
b.cleaner.Do(c)
@ -819,25 +598,31 @@ func (b *Basis) Closer(c func() error) {
// Close is called to clean up resources allocated by the basis.
// This should be called and blocked on to gracefully stop the basis.
func (b *Basis) Close() (err error) {
b.logger.Debug("closing basis",
"basis", b.basis.ResourceId)
b.logger.Debug("closing basis")
// Close down any projects that were loaded
for name, p := range b.projects {
b.logger.Trace("closing project",
"project", name)
if cerr := p.Close(); cerr != nil {
b.logger.Warn("error closing project",
"project", name,
"error", cerr)
err = multierror.Append(err, cerr)
}
return b.cleaner.Close()
}
// Reload basis data
func (b *Basis) Reload() (err error) {
b.m.Lock()
defer b.m.Unlock()
if b.basis.ResourceId == "" {
return status.Error(codes.NotFound, "basis does not exist")
}
if cerr := b.cleaner.Close(); cerr != nil {
err = multierror.Append(err, cerr)
result, err := b.client.FindBasis(b.ctx,
&vagrant_server.FindBasisRequest{
Basis: b.basis,
},
)
if err != nil {
return
}
b.basis = result.Basis
return
}
@ -846,8 +631,7 @@ func (b *Basis) Save() (err error) {
b.m.Lock()
defer b.m.Unlock()
b.logger.Debug("saving basis to db",
"basis", b.basis.ResourceId)
b.logger.Debug("saving basis to db")
result, err := b.Client().UpsertBasis(b.ctx,
&vagrant_server.UpsertBasisRequest{
@ -855,7 +639,6 @@ func (b *Basis) Save() (err error) {
if err != nil {
b.logger.Trace("failed to save basis",
"basis", b.basis.ResourceId,
"error", err)
}
@ -863,32 +646,6 @@ func (b *Basis) Save() (err error) {
return
}
// Saves the basis to the db as well as any projects that have been
// loaded. This will "cascade" to targets as well since `SaveFull` will
// be called on the project.
func (b *Basis) SaveFull() (err error) {
b.logger.Debug("performing full save",
"basis", b.basis.ResourceId)
for _, p := range b.projects {
b.logger.Trace("saving project",
"basis", b.basis.ResourceId,
"project", p.project.ResourceId)
if perr := p.SaveFull(); perr != nil {
b.logger.Trace("error while saving project",
"project", p.project.ResourceId,
"error", err)
err = multierror.Append(err, perr)
}
}
if berr := b.Save(); berr != nil {
err = multierror.Append(err, berr)
}
return
}
func (b *Basis) TargetIndex() (core.TargetIndex, error) {
return b.index, nil
}
@ -906,7 +663,6 @@ func (b *Basis) Components(ctx context.Context) ([]*Component, error) {
// component name. This is the entry point for running commands.
func (b *Basis) Run(ctx context.Context, task *vagrant_server.Task) (err error) {
b.logger.Debug("running new task",
"basis", b,
"task", task)
// Build the component to run
@ -1082,6 +838,123 @@ func (b *Basis) seed(fn func(*core.Seeds)) {
}
}
func (b *Basis) configRegistration(p *plugin.Plugin, l hclog.Logger) error {
if !p.HasType(component.ConfigType) {
b.logger.Warn("plugin does not implement config component type",
"name", p.Name,
)
return nil
}
b.logger.Debug("registering configuration component",
"name", p.Name,
)
i, err := p.Manager().Find(p.Name, component.ConfigType)
if err != nil {
b.logger.Error("failed to load configuration component",
"name", p.Name,
"error", err,
)
return err
}
c, ok := i.Component.(core.Config)
if !ok {
return fmt.Errorf("component instance is not valid config: %s", p.Name)
}
info, err := c.Register()
if err != nil {
b.logger.Error("failed to get registration information from plugin",
"name", p.Name,
"error", err,
)
return err
}
b.logger.Info("registering configuration component",
"plugin", p.Name,
"info", *info,
)
return b.vagrantfile.Register(info, p)
}
func (b *Basis) setPluginCache(i *plugin.Instance, l hclog.Logger) error {
if c, ok := i.Component.(interface {
SetCache(cacher.Cache)
}); ok {
b.logger.Trace("setting cache on plugin instance",
"name", i.Name,
"component", hclog.Fmt("%T", i.Component),
)
c.SetCache(b.cache)
} else {
b.logger.Warn("cannot set cache on plugin instance",
"name", i.Name,
"component", hclog.Fmt("%T", i.Component),
)
}
return nil
}
func (b *Basis) setPluginSeeds(i *plugin.Instance, l hclog.Logger) error {
if s, ok := i.Component.(core.Seeder); ok {
if err := s.Seed(b.seedValues); err != nil {
return err
}
}
return nil
}
func (b *Basis) setPluginManager(i *plugin.Instance, l hclog.Logger) error {
s, ok := i.Component.(plugin.HasPluginMetadata)
if !ok {
l.Warn("plugin does not support metadata, cannot assign plugin manager",
"component", i.Type.String(),
"name", i.Name,
)
return nil
}
srv, err := b.plugins.Servinfo()
if err != nil {
l.Warn("failed to get plugin manager information",
"error", err,
)
return nil
}
s.SetRequestMetadata("plugin_manager", string(srv))
return nil
}
func (b *Basis) setPluginCoreManager(i *plugin.Instance, l hclog.Logger) error {
s, ok := i.Component.(plugin.HasPluginMetadata)
if !ok {
l.Warn("plugin does not support metadata, cannot assign plugin manager",
"component", i.Type.String(),
"name", i.Name,
)
return nil
}
srv, err := b.corePlugins.Servinfo(b.plugins.LegacyBroker())
if err != nil {
l.Warn("failed to get plugin manager information",
"error", err,
)
return nil
}
s.SetRequestMetadata("core_plugin_manager", string(srv))
return nil
}
func (b *Basis) execHook(
ctx context.Context,
log hclog.Logger,
@ -1160,43 +1033,16 @@ func WithBasisDataDir(dir *datadir.Basis) BasisOption {
// WithBasisRef is used to load or initialize the basis
func WithBasisRef(r *vagrant_plugin_sdk.Ref_Basis) BasisOption {
return func(b *Basis) (err error) {
var basis *vagrant_server.Basis
// if we don't have a resource ID we need to upsert
if r.ResourceId == "" {
var result *vagrant_server.UpsertBasisResponse
result, err = b.client.UpsertBasis(
context.Background(),
&vagrant_server.UpsertBasisRequest{
Basis: &vagrant_server.Basis{
Name: r.Name,
},
},
)
if err != nil {
return
}
basis = result.Basis
} else {
var result *vagrant_server.GetBasisResponse
result, err = b.client.GetBasis(
context.Background(),
&vagrant_server.GetBasisRequest{
Basis: r,
},
)
if err != nil {
return
}
basis = result.Basis
if r.ResourceId != "" {
b.basis.ResourceId = r.ResourceId
}
b.basis = basis
// if the datadir isn't set, do that now
if b.dir == nil {
b.dir, err = datadir.NewBasis(basis.Name)
if err != nil {
return
}
if r.Name != "" {
b.basis.Name = r.Name
}
if r.Path != "" {
b.basis.Path = r.Path
}
return
}
}
@ -1241,3 +1087,4 @@ func FromBasis(basis *Basis) BasisOption {
}
var _ core.Basis = (*Basis)(nil)
var _ Scope = (*Basis)(nil)

View File

@ -12,7 +12,6 @@ import (
"github.com/hashicorp/go-argmapper"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-multierror"
goplugin "github.com/hashicorp/go-plugin"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
@ -29,6 +28,7 @@ import (
"github.com/hashicorp/vagrant-plugin-sdk/terminal"
"github.com/hashicorp/vagrant/internal/config"
"github.com/hashicorp/vagrant/internal/plugin"
"github.com/hashicorp/vagrant/internal/server/proto/vagrant_server"
"github.com/hashicorp/vagrant/internal/serverclient"
)
@ -38,46 +38,176 @@ import (
// The Close function should be called when finished with the project
// to properly clean up any open resources.
type Project struct {
project *vagrant_server.Project
ctx context.Context
basis *Basis
logger hclog.Logger
basis *Basis // basis which owns this project
cache cacher.Cache // local project cache
cleanup cleanup.Cleanup // cleanup tasks to be run on close
client *serverclient.VagrantClient // client to vagrant server
ctx context.Context // local context
dir *datadir.Project // data directory for project
factory *Factory // scope factory
jobInfo *component.JobInfo // jobInfo is the base job info for executed functions
logger hclog.Logger // project specific logger
mappers []*argmapper.Func // mappers for project
plugins *plugin.Manager // project scoped plugin manager
project *vagrant_server.Project // stored project data
ready bool // flag that instance is ready
targets map[string]*Target
dir *datadir.Project
mappers []*argmapper.Func
vagrantfile *Vagrantfile
ui terminal.UI // project UI (non-prefixed)
vagrantfile *Vagrantfile // vagrantfile instance for project
// jobInfo is the base job info for executed functions.
jobInfo *component.JobInfo
// This lock only needs to be held currently to protect closers.
m sync.Mutex
// Registered actions for cleanup on close
cleanup cleanup.Cleanup
// UI is the terminal UI to use for messages related to the project
// as a whole. These messages will show up unprefixed for example compared
// to the app-specific UI.
ui terminal.UI
}
// Create a new blank project instance
func NewProject(opts ...ProjectOption) (*Project, error) {
var p *Project
var err error
p = &Project{
cache: cacher.New(),
cleanup: cleanup.New(),
ctx: context.Background(),
logger: hclog.L(),
project: &vagrant_server.Project{},
}
for _, fn := range opts {
if optErr := fn(p); optErr != nil {
err = multierror.Append(err, optErr)
}
}
if err != nil {
return nil, err
}
return p, nil
}
func (p *Project) Init() error {
var err error
// If ready then Init was already run
if p.ready {
return nil
}
// Configure our logger
p.logger = p.logger.ResetNamed("vagrant.core.project")
p.logger = p.logger.With("project", p)
// If the client isn't set, grab it from the basis
if p.client == nil && p.basis != nil {
p.client = p.basis.client
}
// Attempt to reload the project to populate our
// data. If the project is not found, create it.
err = p.Reload()
if err != nil {
stat, ok := status.FromError(err)
if !ok || stat.Code() != codes.NotFound {
return err
}
// Project doesn't exist so save it to persist
if err = p.Save(); err != nil {
return err
}
}
// If we don't have a basis set, load it
if p.basis == nil {
p.basis, err = p.factory.NewBasis(p.project.Basis.ResourceId, WithBasisRef(p.project.Basis))
if err != nil {
return fmt.Errorf("failed to load project basis: %w", err)
}
}
// Set our plugin manager as a sub manager of the basis
p.plugins = p.basis.plugins.Sub("project")
// If our project closes early, close the plugin sub manager
// so it isn't just hanging around
p.Closer(func() error {
return p.plugins.Close()
})
// Always ensure the basis reference is set
p.project.Basis = p.basis.Ref().(*vagrant_plugin_sdk.Ref_Basis)
// If the project directory is unset, set it
if p.dir == nil {
if p.dir, err = p.basis.dir.Project(p.project.Name); err != nil {
return err
}
}
// If the ui is unset, use basis ui
if p.ui == nil {
p.ui = p.basis.ui
}
// Load any plugins that may be installed locally to the project
if err = p.plugins.Discover(path.NewPath(p.project.Path).Join(".vagrant").Join("plugins")); err != nil {
p.logger.Error("project setup failed during plugin discovery",
"directory", path.NewPath(p.project.Path).Join(".vagrant").Join("plugins"),
"error", err,
)
return err
}
// Clone our vagrantfile to use in the new project
v := p.basis.vagrantfile.clone("project")
v.logger = p.logger.Named("vagrantfile")
// Add the project vagrantfile
err = v.Source(p.project.Configuration, VAGRANTFILE_PROJECT)
if err != nil {
return err
}
// Init the vagrantfile so the config is available
if err = v.Init(); err != nil {
return err
}
p.vagrantfile = v
// Store our configuration
sv, err := v.GetSource(VAGRANTFILE_PROJECT)
if err != nil {
return err
}
p.project.Configuration = sv
// Set our ref into vagrantfile
p.vagrantfile.targetSource = p.Ref().(*vagrant_plugin_sdk.Ref_Project)
// Set project seeds
p.seed(nil)
// Remove any stale targets
if err = p.scrubTargets(); err != nil {
return err
}
// Save ourself when closed
p.Closer(func() error {
return p.Save()
})
// Set flag that this instance is setup
p.ready = true
p.logger.Info("project initialized")
return nil
}
// Provide nice output in logger
func (p *Project) String() string {
return fmt.Sprintf("core.Project:[basis: %s, name: %s, resource_id: %s, address: %p]",
p.basis.Name(), p.project.Name, p.project.ResourceId, p)
return fmt.Sprintf("core.Project:[basis: %v, name: %s, resource_id: %s, address: %p]",
p.basis, p.project.Name, p.project.ResourceId, p)
}
// Cache implements originScope
func (p *Project) Cache() cacher.Cache {
return p.basis.cache
}
// Broker implements originScope
func (p *Project) Broker() *goplugin.GRPCBroker {
return p.basis.plugins.LegacyBroker()
}
// Vagrantfile implements originScope
// Vagrantfile implements core.Project
func (p *Project) Vagrantfile() (core.Vagrantfile, error) {
return p.vagrantfile, nil
}
@ -167,23 +297,30 @@ func (p *Project) DefaultProvider(opts *core.DefaultProviderOptions) (string, er
}
for _, n := range targets {
targetConfig, err := p.vagrantfile.TargetConfig(n, "", false)
target, err := p.Target(n, "")
if err != nil {
return "", err
return "", nil
}
tv := targetConfig.(*Vagrantfile)
if target.(*Target).target.Provider != "" {
configProviders = append(configProviders, target.(*Target).target.Provider)
} else {
tv := target.(*Target).vagrantfile
pRaw, err := tv.GetValue("vm", "__provider_order")
providers, ok := pRaw.([]interface{})
if !ok {
return "", fmt.Errorf("unexpected type for target provider list (%T)", pRaw)
}
for _, pint := range providers {
pstring, err := optionToString(pint)
pRaw, err := tv.GetValue("vm", "__provider_order")
if err != nil {
return "", fmt.Errorf("unexpected type for target provider (%T)", pint)
continue
}
providers, ok := pRaw.([]interface{})
if !ok {
return "", fmt.Errorf("unexpected type for target provider list (%T)", pRaw)
}
for _, pint := range providers {
pstring, err := optionToString(pint)
if err != nil {
return "", fmt.Errorf("unexpected type for target provider (%T)", pint)
}
configProviders = append(configProviders, pstring)
}
configProviders = append(configProviders, pstring)
}
}
@ -353,8 +490,7 @@ func (p *Project) LocalData() (d path.Path, err error) {
// PrimaryTargetName implements core.Project
func (p *Project) PrimaryTargetName() (name string, err error) {
// TODO: This needs the Vagrantfile service to be implemented
return
return p.vagrantfile.PrimaryTargetName()
}
// Resource implements core.Project
@ -363,21 +499,16 @@ func (p *Project) ResourceId() (string, error) {
}
// RootPath implements core.Project
func (p *Project) RootPath() (path path.Path, err error) {
// TODO: need vagrantfile loading to be completed in order to implement
return
func (p *Project) RootPath() (path.Path, error) {
return path.NewPath(p.project.Configuration.Path.Path), nil
}
func (p *Project) Factory() *Factory {
return p.basis.factory
}
// Target implements core.Project
func (p *Project) Target(nameOrId string, provider string) (core.Target, error) {
// TODO(spox): do we need to add a check here if the
// already loaded target doesn't match the
// provided provider name?
if t, ok := p.targets[nameOrId]; ok {
return t, nil
}
return p.vagrantfile.Target(nameOrId, provider)
}
@ -416,14 +547,19 @@ func (p *Project) UI() (terminal.UI, error) {
// Targets
func (p *Project) Targets() ([]core.Target, error) {
var targets []core.Target
for _, ref := range p.project.Targets {
t, err := p.LoadTarget(WithTargetRef(ref))
names, err := p.TargetNames()
if err != nil {
return nil, err
}
targets := make([]core.Target, len(names))
for i, n := range names {
t, err := p.Target(n, "")
if err != nil {
return nil, err
}
targets = append(targets, t)
targets[i] = t
}
return targets, nil
}
@ -437,103 +573,9 @@ func (p *Project) JobInfo() *component.JobInfo {
return p.jobInfo
}
// LoadTarget loads a target within the current project. If the target is not
// found, it will be created.
func (p *Project) LoadTarget(topts ...TargetOption) (*Target, error) {
p.m.Lock()
defer p.m.Unlock()
// Create our target
t := &Target{
cache: cacher.New(),
ctx: p.ctx,
project: p,
logger: p.logger,
target: &vagrant_server.Target{
Project: p.Ref().(*vagrant_plugin_sdk.Ref_Project),
},
ui: p.ui,
}
var err error
// Apply any options provided
for _, opt := range topts {
if oerr := opt(t); oerr != nil {
err = multierror.Append(err, oerr)
}
}
if err != nil {
return nil, err
}
// Lookup target in cached list by name
if c, ok := p.targets[t.target.Name]; ok {
return c, nil
}
// Lookup target in cached list by resource id
if c, ok := p.targets[t.target.ResourceId]; ok {
return c, nil
}
// If we don't have a vagrantfile assigned to
// this target, request it and set it
if t.vagrantfile == nil {
p.logger.Info("target does not have vagrantfile set, loading", "target", t.target.Name)
tv, err := p.vagrantfile.TargetConfig(t.target.Name, "", false)
if err != nil {
return nil, err
}
// Set the vagrantfile if one was returned
if tv != nil {
t.vagrantfile = tv.(*Vagrantfile)
}
}
// If this is the first time through, re-init the target
if err = t.init(); err != nil {
return nil, err
}
// If the data directory is set, set it
if t.dir == nil {
if t.dir, err = p.dir.Target(t.target.Name); err != nil {
return nil, err
}
}
// Update the logger name based on the level
if t.logger.IsTrace() {
t.logger = t.logger.Named("target")
} else {
t.logger = t.logger.ResetNamed("vagrant.core.target")
}
// Ensure any modifications to the target are persisted
t.Closer(func() error { return t.Save() })
// Remove the target from the list when closed
t.Closer(func() error {
delete(p.targets, t.target.ResourceId)
return nil
})
// Close the target when the project is closed
p.Closer(func() error {
return t.Close()
})
// Add the target to target list in project
p.targets[t.target.ResourceId] = t
p.targets[t.target.Name] = t
return t, nil
}
// Client returns the API client for the backend server.
func (p *Project) Client() *serverclient.VagrantClient {
return p.basis.client
return p.client
}
// Ref returns the project ref for API calls.
@ -547,9 +589,13 @@ func (p *Project) Ref() interface{} {
func (p *Project) Run(ctx context.Context, task *vagrant_server.Task) (err error) {
p.logger.Debug("running new task",
"project", p,
"task", task)
// Initialize our targets before running
if err = p.InitTargets(); err != nil {
return
}
cmd, err := p.basis.component(
ctx, component.CommandType, task.Component.Name)
if err != nil {
@ -590,7 +636,10 @@ func (p *Project) Run(ctx context.Context, task *vagrant_server.Task) (err error
return
}
func (p *Project) seed(fn func(*core.Seeds)) {
// Set project specific seeds
func (p *Project) seed(
fn func(*core.Seeds), // callback for adding seeds
) {
p.basis.seed(
func(s *core.Seeds) {
s.AddNamed("project", p)
@ -611,23 +660,34 @@ func (p *Project) Closer(c func() error) {
// Close is called to clean up resources allocated by the project.
// This should be called and blocked on to gracefully stop the project.
func (p *Project) Close() (err error) {
p.logger.Debug("closing project",
"project", p)
// Remove this project from basis project list
delete(p.basis.projects, p.Name())
delete(p.basis.projects, p.project.ResourceId)
p.logger.Trace("closing project")
return p.cleanup.Close()
}
// Saves the project to the db
func (p *Project) Save() (err error) {
func (p *Project) Save() error {
p.m.Lock()
defer p.m.Unlock()
p.logger.Trace("saving project to db",
"project", p.project.ResourceId)
p.logger.Trace("saving project to db")
// Remove the defined vms from finalized data to
// prevent it from being used on subsequent runs
if err := p.vagrantfile.DeleteValue("vm", "__defined_vms"); err != nil {
p.logger.Warn("failed to remove defined vms configuration before save",
"error", err,
)
}
val, err := p.vagrantfile.rootToStore()
if err != nil {
p.logger.Warn("failed to convert modified configuration for save",
"error", err,
)
} else {
p.project.Configuration.Finalized = val.Data
}
result, err := p.Client().UpsertProject(p.ctx,
&vagrant_server.UpsertProjectRequest{
@ -636,132 +696,110 @@ func (p *Project) Save() (err error) {
)
if err != nil {
p.logger.Trace("failed to save project",
"project", p.project.ResourceId)
"error", err,
)
return err
}
p.project = result.Project
return
}
// Saves the project to the db as well as any targets that have been loaded
func (p *Project) SaveFull() (err error) {
p.logger.Debug("performing full save",
"project", p.project.ResourceId)
for _, t := range p.targets {
p.logger.Trace("saving target",
"project", p.project.ResourceId,
"target", t.target.ResourceId)
if terr := t.Save(); terr != nil {
p.logger.Trace("error while saving target",
"target", t.target.ResourceId,
"error", err)
err = multierror.Append(err, terr)
}
}
if perr := p.Save(); perr != nil {
err = multierror.Append(err, perr)
}
return
return nil
}
func (p *Project) Components(ctx context.Context) ([]*Component, error) {
return p.basis.components(ctx)
}
func (p *Project) InitTargets() (err error) {
p.m.Lock()
defer p.m.Unlock()
defer func() {
if err != nil {
p.logger.Error("failed to initialize targets",
"error", err,
)
}
}()
p.logger.Trace("initializing targets defined within project",
"project", p.Name())
targets, err := p.vagrantfile.TargetNames()
func (p *Project) scrubTargets() (err error) {
var updated bool
targets, err := p.TargetNames()
if err != nil {
return
return err
}
current := map[string]struct{}{}
for _, name := range targets {
current[name] = struct{}{}
}
if len(targets) == 0 {
p.logger.Trace("no targets defined within current project",
"project", p.Name())
return
}
// Get list of all currently known targets for project
var existingTargets []string
for _, t := range p.project.Targets {
existingTargets = append(existingTargets, t.Name)
}
p.logger.Trace("targets associated with project",
"project", p,
"existing", existingTargets,
"defined", targets,
)
updated := false
seen := map[string]struct{}{}
for _, t := range targets {
_, err = p.createTarget(t)
if err != nil {
p.logger.Error("failed to initialize target with project",
"project", p.Name(),
"target", t,
"error", err,
)
return
}
seen[t] = struct{}{}
updated = true
}
// If any existing targets are not in the defined list and are
// not in a created state, delete them as they were removed
// from the vagrantfile
for _, existName := range existingTargets {
if _, ok := seen[existName]; ok {
if _, ok := current[t.Name]; ok {
continue
}
resp, err := p.Client().FindTarget(p.ctx,
&vagrant_server.FindTargetRequest{
Target: &vagrant_server.Target{
Name: existName,
Project: p.Ref().(*vagrant_plugin_sdk.Ref_Project),
},
resp, err := p.client.GetTarget(p.ctx,
&vagrant_server.GetTargetRequest{
Target: t,
},
)
if err != nil {
return err
}
// If the state is not created or unknown, remove it
if resp.Target.State == vagrant_server.Operation_NOT_CREATED ||
resp.Target.State == vagrant_server.Operation_UNKNOWN {
_, err := p.Client().DeleteTarget(p.ctx,
resp.Target.State == vagrant_server.Operation_UNKNOWN ||
resp.Target.State == vagrant_server.Operation_DESTROYED {
_, err = p.client.DeleteTarget(p.ctx,
&vagrant_server.DeleteTargetRequest{
Target: &vagrant_plugin_sdk.Ref_Target{
Name: existName,
ResourceId: resp.Target.ResourceId,
Project: p.Ref().(*vagrant_plugin_sdk.Ref_Project),
},
Target: t,
},
)
if err != nil && status.Code(err) != codes.NotFound {
if err != nil {
return err
}
updated = true
}
}
if updated {
err = p.Reload()
}
return
}
// Initialize all targets for this project
func (p *Project) InitTargets() (err error) {
p.logger.Trace("initializing targets defined within project")
var updated bool
targets, err := p.Targets()
if err != nil {
return
}
current := map[string]struct{}{}
for _, t := range targets {
rid, err := t.ResourceId()
if err != nil {
return err
}
current[rid] = struct{}{}
}
// Cycle over targets registered to project and if any
// are not in an "exist" type state, delete them as
// they were removed from the vagrantfile
for _, t := range p.project.Targets {
if _, ok := current[t.ResourceId]; ok {
continue
}
target, err := p.Factory().NewTarget(
WithProject(p),
WithTargetRef(t),
)
if err != nil {
return err
}
state, err := target.State()
if err != nil {
return err
}
if state == core.NOT_CREATED || state == core.UNKNOWN {
if err = target.Destroy(); err != nil {
return err
} else {
err = nil
}
updated = true
}
@ -771,26 +809,27 @@ func (p *Project) InitTargets() (err error) {
// If targets have been updated then refresh the project. This is required
// since upserting targets will also update the project to have a reference
// to the new targets.
err = p.refreshProject()
err = p.Reload()
}
return
}
// Get's the latest project from the DB
func (p *Project) refreshProject() (err error) {
// Reload the project data
func (p *Project) Reload() (err error) {
p.m.Lock()
defer p.m.Unlock()
if p.project.ResourceId == "" {
return status.Error(codes.NotFound, "project does not exist")
}
result, err := p.Client().FindProject(p.ctx,
&vagrant_server.FindProjectRequest{
Project: &vagrant_server.Project{
ResourceId: p.project.ResourceId,
},
Project: p.project,
},
)
if err != nil {
p.logger.Error("failed to refresh project data",
"project", p.Name(),
"error", err,
)
if err != nil {
return
}
@ -881,12 +920,20 @@ type ProjectOption func(*Project) error
func WithBasis(b *Basis) ProjectOption {
return func(p *Project) (err error) {
p.basis = b
p.project.Basis = b.Ref().(*vagrant_plugin_sdk.Ref_Basis)
// NOTE: only set the UI if it's unset
if p.ui == nil {
p.ui = b.ui
}
return
}
}
func WithProjectDataDir(dir *datadir.Project) ProjectOption {
return func(p *Project) (err error) {
if dir == nil {
return errors.New("directory value cannot be nil")
}
p.dir = dir
return
}
@ -894,37 +941,10 @@ func WithProjectDataDir(dir *datadir.Project) ProjectOption {
func WithProjectName(name string) ProjectOption {
return func(p *Project) (err error) {
if p.basis == nil {
return errors.New("basis must be set before loading project")
if name == "" {
return errors.New("name cannot be empty")
}
if ex := p.basis.Project(name); ex != nil {
p.project = ex.project
return
}
var match *vagrant_plugin_sdk.Ref_Project
for _, m := range p.basis.basis.Projects {
if m.Name == name {
match = m
break
}
}
if match == nil {
return errors.New("project is not registered in basis")
}
result, err := p.Client().FindProject(p.ctx, &vagrant_server.FindProjectRequest{
Project: &vagrant_server.Project{Name: name},
})
if err != nil {
return
}
if result == nil {
p.logger.Error("failed to locate project during setup", "project", name,
"basis", p.basis.Ref())
return errors.New("failed to load project")
}
p.project = result.Project
p.project.Name = name
return
}
}
@ -936,57 +956,22 @@ func WithProjectRef(r *vagrant_plugin_sdk.Ref_Project) ProjectOption {
if r == nil {
return errors.New("project reference cannot be nil")
}
// Basis must be set before we continue
if p.basis == nil {
return errors.New("basis must be set before loading project")
if r.Name != "" {
p.project.Name = r.Name
}
var project *vagrant_server.Project
// Check if the basis has already loaded the project. If so,
// then initialize on that project
if ex := p.basis.projects[r.Name]; ex != nil {
project = ex.project
return
if r.Path != "" {
p.project.Path = r.Path
}
result, err := p.Client().FindProject(p.ctx,
&vagrant_server.FindProjectRequest{
Project: &vagrant_server.Project{
Basis: r.Basis,
Name: r.Name,
Path: r.Path,
ResourceId: r.ResourceId,
},
},
)
if err != nil {
var result *vagrant_server.UpsertProjectResponse
result, err = p.Client().UpsertProject(p.ctx,
&vagrant_server.UpsertProjectRequest{
Project: &vagrant_server.Project{
Name: r.Name,
Path: r.Name,
Basis: r.Basis,
},
},
)
if err != nil {
return
}
project = result.Project
} else {
project = result.Project
if r.ResourceId != "" {
p.project.ResourceId = r.ResourceId
}
// Before we init, validate basis is consistent
if r.Basis != nil && project.Basis.ResourceId != r.Basis.ResourceId {
p.logger.Error("invalid basis for project", "request-basis", r.Basis,
"project-basis", project.Basis)
return errors.New("project basis configuration is invalid")
if r.Basis != nil {
p.project.Basis = r.Basis
}
p.project = project
return
}
}
var _ core.Project = (*Project)(nil)
var _ Scope = (*Project)(nil)

View File

@ -13,7 +13,8 @@ import (
"github.com/hashicorp/go-argmapper"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-multierror"
goplugin "github.com/hashicorp/go-plugin"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"
@ -21,11 +22,10 @@ import (
"github.com/hashicorp/vagrant-plugin-sdk/core"
"github.com/hashicorp/vagrant-plugin-sdk/datadir"
"github.com/hashicorp/vagrant-plugin-sdk/internal-shared/cacher"
"github.com/hashicorp/vagrant-plugin-sdk/internal-shared/dynamic"
"github.com/hashicorp/vagrant-plugin-sdk/internal-shared/cleanup"
"github.com/hashicorp/vagrant-plugin-sdk/proto/vagrant_plugin_sdk"
"github.com/hashicorp/vagrant-plugin-sdk/terminal"
"github.com/hashicorp/vagrant/internal/config"
"github.com/hashicorp/vagrant/internal/plugin"
"github.com/hashicorp/vagrant/internal/server/proto/vagrant_server"
"github.com/hashicorp/vagrant/internal/serverclient"
)
@ -33,23 +33,147 @@ import (
const DEFAULT_COMMUNICATOR_NAME = "ssh"
type Target struct {
ctx context.Context
target *vagrant_server.Target
project *Project
logger hclog.Logger
dir *datadir.Target
cache cacher.Cache // local target cache
cleanup cleanup.Cleanup // cleanup tasks to be run on close
client *serverclient.VagrantClient // client to vagrant server
ctx context.Context // local target context
dir *datadir.Target // data directory for target
factory *Factory // scope factory
jobInfo *component.JobInfo // jobInfo is the base job info for executed functions
logger hclog.Logger // target specific logger
project *Project // project which owns this target
ready bool // flag that instance is ready
target *vagrant_server.Target // stored target data
ui terminal.UI // target UI
vagrantfile *Vagrantfile // vagrantfile instance for target
m sync.Mutex
jobInfo *component.JobInfo
closers []func() error
ui terminal.UI
cache cacher.Cache
vagrantfile *Vagrantfile
m sync.Mutex
}
func NewTarget(opts ...TargetOption) (*Target, error) {
var t *Target
var err error
t = &Target{
cache: cacher.New(),
cleanup: cleanup.New(),
ctx: context.Background(),
logger: hclog.L(),
target: &vagrant_server.Target{},
}
for _, fn := range opts {
if optErr := fn(t); optErr != nil {
err = multierror.Append(err, optErr)
}
}
if err != nil {
return nil, err
}
return t, err
}
func (t *Target) Init() error {
var err error
// If ready then Init was already run
if t.ready {
return nil
}
// Configure our logger
t.logger = t.logger.ResetNamed("vagrant.core.target")
t.logger = t.logger.With("target", t)
// If no client is set, grab it from the project
if t.client == nil && t.project != nil {
t.client = t.project.client
}
// Attempt to reload the target to populate our
// data. If the target is not found, create it.
err = t.Reload()
if err != nil {
stat, ok := status.FromError(err)
if !ok || stat.Code() != codes.NotFound {
return err
}
// Target doesn't exist so save it to persist
if err = t.Save(); err != nil {
return err
}
}
// If we don't have a project set, load it
if t.project == nil {
t.project, err = t.factory.NewProject(WithProjectRef(t.target.Project))
if err != nil {
return fmt.Errorf("failed to load target project: %w", err)
}
}
// Always ensure the project reference is set
t.target.Project = t.project.Ref().(*vagrant_plugin_sdk.Ref_Project)
// If the target directory is unset, set it
if t.dir == nil {
if t.dir, err = t.project.dir.Target(t.target.Name); err != nil {
return err
}
}
// If the ui is unset, use the project ui
if t.ui == nil {
t.ui = t.project.ui
}
// Save ourself when closed
t.Closer(func() error {
return t.Save()
})
// If we have a vagrantfile set, we are done
if t.vagrantfile != nil {
t.vagrantfile.logger = t.logger.Named("vagrantfile")
return nil
}
// We don't have a vagrantfile set so we need to restore
// our stored configuration. First, make sure we have some
// store configuration!
if t.target.Configuration == nil {
t.target.Configuration = &vagrant_plugin_sdk.Args_ConfigData{}
// Since we don't have any data to load, just stub and return
t.vagrantfile = t.project.vagrantfile.clone("target")
t.vagrantfile.root = &component.ConfigData{
Data: map[string]interface{}{},
}
return nil
}
v := t.project.vagrantfile.clone("target")
v.logger = t.logger.Named("vagrantfile")
if err = v.loadToRoot(t.target.Configuration); err != nil {
return err
}
t.vagrantfile = v
// Set flag that this instance is setup
t.ready = true
t.logger.Info("target initialized")
return nil
}
func (t *Target) String() string {
return fmt.Sprintf("core.Target[basis: %s, project: %s, resource_id: %s, address: %p]",
t.project.basis.Name(), t.project.Name(), t.target.ResourceId, t,
return fmt.Sprintf("core.Target[basis: %s, project: %s, resource_id: %s, name: %s, address: %p]",
t.project.basis.Name(), t.project.Name(), t.target.ResourceId, t.target.Name, t,
)
}
@ -247,29 +371,37 @@ func (t *Target) JobInfo() *component.JobInfo {
// Client returns the API client for the backend server.
func (t *Target) Client() *serverclient.VagrantClient {
return t.project.basis.client
return t.client
}
func (t *Target) Closer(c func() error) {
t.closers = append(t.closers, c)
t.cleanup.Do(c)
}
// Close is called to clean up resources allocated by the target.
// This should be called and blocked on to gracefully stop the target.
func (t *Target) Close() (err error) {
t.logger.Debug("closing target",
"target", t)
t.logger.Debug("closing target")
for _, c := range t.closers {
if cerr := c(); cerr != nil {
t.logger.Warn("error executing closer",
"error", cerr)
return t.cleanup.Close()
}
err = multierror.Append(err, cerr)
}
// Reload the target data
func (t *Target) Reload() (err error) {
t.m.Lock()
defer t.m.Unlock()
result, err := t.Client().FindTarget(t.ctx,
&vagrant_server.FindTargetRequest{
Target: t.target,
},
)
if err != nil {
return
}
// Remove this target from built target list in project
delete(t.project.targets, t.target.Name)
t.target = result.Target
return
}
@ -278,16 +410,23 @@ func (t *Target) Save() (err error) {
t.m.Lock()
defer t.m.Unlock()
t.logger.Debug("saving target to db",
"target", t.target.ResourceId,
"name", t.target.Name,
)
t.logger.Debug("saving target to db")
// If there were any modification to the configuration
// after init, be sure we capture them
t.target.Configuration, err = t.vagrantfile.rootToStore()
if err != nil {
t.logger.Warn("failed to serialize configuration prior to save",
"error", err,
)
// Only warn since we want to save whatever information we can
err = nil
}
result, uerr := t.Client().UpsertTarget(t.ctx, &vagrant_server.UpsertTargetRequest{
Target: t.target})
if uerr != nil {
t.logger.Trace("failed to save target",
"target", t.target.ResourceId,
"error", uerr)
err = multierror.Append(err, uerr)
@ -299,9 +438,13 @@ func (t *Target) Save() (err error) {
}
func (t *Target) Destroy() (err error) {
t.Close()
t.m.Lock()
defer t.m.Unlock()
// Run all the cleanup tasks on the target
t.Close()
// Delete the target from the database
_, err = t.Client().DeleteTarget(t.ctx, &vagrant_server.DeleteTargetRequest{
Target: t.Ref().(*vagrant_plugin_sdk.Ref_Target),
})
@ -317,20 +460,15 @@ func (t *Target) Destroy() (err error) {
err = multierror.Append(err, rerr)
}
}
t.target = &vagrant_server.Target{}
return
}
func (t *Target) Run(ctx context.Context, task *vagrant_server.Task) (err error) {
t.logger.Debug("running new task",
"target", t,
"task", task)
// Intialize targets
if err = t.project.InitTargets(); err != nil {
return err
}
cmd, err := t.project.basis.component(
ctx, component.CommandType, task.Component.Name)
@ -369,31 +507,11 @@ func (t *Target) Run(ctx context.Context, task *vagrant_server.Task) (err error)
return
}
// LoadTarget implements originScope
func (t *Target) LoadTarget(topts ...TargetOption) (*Target, error) {
return nil, fmt.Errorf("targets cannot be loaded from a target")
}
// Boxes implements originScope
func (t *Target) Boxes() (core.BoxCollection, error) {
return nil, fmt.Errorf("boxes cannot be loaded from a target")
}
// Vagrantfile implements originScope / core.Target
// Vagrantfile implements core.Target
func (t *Target) Vagrantfile() (core.Vagrantfile, error) {
return t.vagrantfile, nil
}
// Cache implements originScope
func (t *Target) Cache() cacher.Cache {
return t.project.basis.cache
}
// Broker implements originScope
func (t *Target) Broker() *goplugin.GRPCBroker {
return t.project.basis.plugins.LegacyBroker()
}
func (t *Target) seed(fn func(*core.Seeds)) {
t.project.seed(
func(s *core.Seeds) {
@ -418,7 +536,7 @@ func (t *Target) Machine() core.Machine {
t.target.Record.UnmarshalTo(targetMachine)
m := &Machine{
Target: t,
logger: t.logger,
logger: t.logger.Named("machine"),
machine: targetMachine,
cache: cacher.New(),
vagrantfile: t.vagrantfile,
@ -471,104 +589,16 @@ func (t *Target) doOperation(
return doOperation(ctx, log, t, op)
}
// Initialize the target instance
func (t *Target) init() (err error) {
// As long as no error is encountered,
// update the target configuration.
defer func() {
if err == nil {
t.target.Configuration, err = t.vagrantfile.rootToStore()
}
}()
t.logger.Info("running init on target", "target", t.target.Name)
// Name or resource id is required for a target to be loaded
if t.target.Name == "" && t.target.ResourceId == "" {
return fmt.Errorf("cannot load a target without name or resource id")
}
// A parent project is also required
if t.project == nil {
return fmt.Errorf("cannot load a target without defined project")
}
// If the configuration was updated during load, save it so
// we can re-apply after loading stored data
var conf *vagrant_plugin_sdk.Args_ConfigData
if t.target.Configuration != nil {
conf = t.target.Configuration
}
// Pull target info
resp, err := t.Client().FindTarget(t.ctx,
&vagrant_server.FindTargetRequest{
Target: t.target,
},
)
if err != nil {
return
}
t.target = resp.Target
// If we have configuration data, re-apply it
if conf != nil {
t.target.Configuration = conf
}
// Set the project into the target
t.target.Project = t.project.Ref().(*vagrant_plugin_sdk.Ref_Project)
// If we have a vagrantfile attached, we're done
if t.vagrantfile != nil {
return
}
// If we don't have configuration data, just stub
if t.target.Configuration == nil {
t.target.Configuration = &vagrant_plugin_sdk.Args_ConfigData{}
t.vagrantfile = t.project.vagrantfile.clone("target", t)
t.vagrantfile.root = &component.ConfigData{
Data: map[string]interface{}{},
}
return
}
t.logger.Info("vagrantfile has not been defined so generating from store config",
"name", t.target.Name,
)
internal := plugin.NewInternal(
t.project.basis.plugins.LegacyBroker(),
t.project.basis.cache,
t.project.basis.cleaner,
t.logger,
t.project.basis.mappers,
)
// Load the configuration data we have
raw, err := dynamic.Map(
t.target.Configuration,
(**component.ConfigData)(nil),
argmapper.ConverterFunc(t.project.basis.mappers...),
argmapper.Typed(
t.ctx,
t.logger,
internal,
),
)
if err != nil {
return
}
t.vagrantfile = t.project.vagrantfile.clone("target", t)
t.vagrantfile.root = raw.(*component.ConfigData)
return
}
// Options type for target loading
type TargetOption func(*Target) error
func WithProject(p *Project) TargetOption {
return func(t *Target) (err error) {
t.project = p
return
}
}
// Set a vagrantfile instance on target
func WithTargetVagrantfile(v *Vagrantfile) TargetOption {
return func(t *Target) (err error) {
@ -587,34 +617,18 @@ func WithTargetName(name string) TargetOption {
// Configure target with proto ref
func WithTargetRef(r *vagrant_plugin_sdk.Ref_Target) TargetOption {
return func(t *Target) error {
// Target ref must include a resource id or name
if r.Name == "" && r.ResourceId == "" {
return fmt.Errorf("target ref must include ResourceId and/or Name")
return func(t *Target) (err error) {
if r.Name != "" {
t.target.Name = r.Name
}
if r.ResourceId != "" {
t.target.ResourceId = r.ResourceId
}
if r.Project != nil {
t.target.Project = r.Project
}
// Target ref must include project ref if resource id is empty
if r.Name == "" && r.Project == nil {
return fmt.Errorf("target ref must include Project for name lookup")
}
result, err := t.Client().FindTarget(t.ctx,
&vagrant_server.FindTargetRequest{
Target: &vagrant_server.Target{
ResourceId: r.ResourceId,
Name: r.Name,
Project: r.Project,
},
},
)
if err != nil {
return err
}
t.target = result.Target
return nil
return
}
}
@ -628,3 +642,4 @@ func WithProvider(provider string) TargetOption {
}
var _ core.Target = (*Target)(nil)
var _ Scope = (*Target)(nil)