From 51d8c847405a9762572abcf4ffaef5ca6217cae9 Mon Sep 17 00:00:00 2001 From: Chris Roberts Date: Wed, 6 Jul 2022 12:53:36 -0700 Subject: [PATCH] Refactor how scopes are created and initialized --- internal/core/basis.go | 785 ++++++++++++++++----------------------- internal/core/project.go | 691 +++++++++++++++++----------------- internal/core/target.go | 381 ++++++++++--------- 3 files changed, 852 insertions(+), 1005 deletions(-) diff --git a/internal/core/basis.go b/internal/core/basis.go index 4bdee0244..b3305b012 100644 --- a/internal/core/basis.go +++ b/internal/core/basis.go @@ -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) diff --git a/internal/core/project.go b/internal/core/project.go index de693fdb6..fcfb1e240 100644 --- a/internal/core/project.go +++ b/internal/core/project.go @@ -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) diff --git a/internal/core/target.go b/internal/core/target.go index 74d6ff887..2abfbecc4 100644 --- a/internal/core/target.go +++ b/internal/core/target.go @@ -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)