diff --git a/internal/core/basis.go b/internal/core/basis.go index 64918e7c9..3b167ec4a 100644 --- a/internal/core/basis.go +++ b/internal/core/basis.go @@ -2,6 +2,7 @@ package core import ( "context" + "errors" "fmt" "reflect" "strings" @@ -11,6 +12,7 @@ import ( "github.com/hashicorp/go-argmapper" "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-multierror" + "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -34,23 +36,30 @@ import ( // finished with the basis to properly clean // up any open resources. type Basis struct { - name string - resourceid string - logger hclog.Logger - config *config.Config - projects map[string]*Project - factories map[component.Type]*factory.Factory - mappers []*argmapper.Func - dir *datadir.Basis - env *Environment - - labels map[string]string - overrideLabels map[string]string + basis *vagrant_server.Basis + logger hclog.Logger + config *config.Config + projects map[string]*Project + factories map[component.Type]*factory.Factory + mappers []*argmapper.Func + dir *datadir.Basis + ctx context.Context + lock sync.Mutex client *serverclient.VagrantClient + // NOTE: we can't have a broker to easily just shove at the + // mappers to help us. instead, lets update our scope defs + // in the proto Args so they can be a stream id (when being + // wrapped/passed by a plugin) or a connection end point. This + // will let us serve it from here without a new process being + // spun out, and we can wrap as needed as it gets shuttled around. + // Will just need a direct connection end point like we had for the + // ruby side before the broker there, and the mapper can just use + // what ever is available when doing setup. + grpcServer *grpc.Server + jobInfo *component.JobInfo - lock sync.Mutex closers []func() error UI terminal.UI } @@ -58,24 +67,43 @@ type Basis struct { // NewBasis creates a new Basis with the given options. func NewBasis(ctx context.Context, opts ...BasisOption) (b *Basis, err error) { b = &Basis{ + ctx: ctx, logger: hclog.L(), jobInfo: &component.JobInfo{}, factories: plugin.BaseFactories, } for _, opt := range opts { - opt(b) + if oerr := opt(b); oerr != nil { + err = multierror.Append(err, oerr) + } + } + + if err != nil { + return + } + + if b.basis == nil { + return nil, errors.New("basis data was not properly loaded") + } + + // Client is required to be provided + if b.client == nil { + return nil, errors.New("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 UI was provided, initialize a console UI if b.UI == nil { b.UI = terminal.ConsoleUI(ctx) } + // If the mappers aren't already set, load known mappers if len(b.mappers) == 0 { b.mappers, err = argmapper.NewFuncList(protomappers.All, argmapper.Logger(b.logger), @@ -84,28 +112,29 @@ func NewBasis(ctx context.Context, opts ...BasisOption) (b *Basis, err error) { return } } - - envMapper, _ := argmapper.NewFunc(EnvironmentProto) - b.mappers = append(b.mappers, envMapper) - comandArgMapper, _ := argmapper.NewFunc(CommandArgToMap) b.mappers = append(b.mappers, comandArgMapper) - if b.client == nil { - panic("b.client should never be nil") - } - + // TODO(spox): After fixing up datadir, use that to do + // configuration loading if b.config == nil { - b.config, err = config.Load("", "") + if b.config, err = config.Load("", ""); err != nil { + return + } } - b.env, err = NewEnvironment(ctx, - WithHomePath(b.dir.Dir.RootDir()), - WithServerAddr(b.client.ServerTarget()), - ) - if err != nil { - return nil, err - } + // Finally, init a server to make this basis available + b.grpcServer = grpc.NewServer([]grpc.ServerOption{}...) + vagrant_plugin_sdk.RegisterTargetServiceServer(b.grpcServer, b) + + // Close down the local server when finished with the basis + b.Closer(func() error { + b.grpcServer.GracefulStop() + return nil + }) + + // Ensure any modifications to the basis are persisted + b.Closer(func() error { b.Save() }) b.logger.Info("basis initialized") return @@ -116,12 +145,28 @@ func (b *Basis) Ui() terminal.UI { } func (b *Basis) Ref() interface{} { - return &vagrant_server.Ref_Basis{ - ResourceId: b.resourceid, - Name: b.name, + return &vagrant_plugin_sdk.Ref_Basis{ + ResourceId: b.ResourceId(), + Name: b.Name(), } } +func (b *Basis) Name() string { + if b.basis == nil { + return "" + } + + return b.basis.Name +} + +func (b *Basis) ResourceId() string { + if b.basis == nil { + return "" + } + + return b.basis.ResourceId +} + func (b *Basis) JobInfo() *component.JobInfo { return b.jobInfo } @@ -130,10 +175,6 @@ func (b *Basis) Client() *serverclient.VagrantClient { return b.client } -func (b *Basis) Environment() *Environment { - return b.env -} - func (b *Basis) Init() (result *vagrant_server.Job_InitResult, err error) { b.logger.Debug("running init for basis") f := b.factories[component.CommandType] @@ -153,6 +194,7 @@ func (b *Basis) Init() (result *vagrant_server.Job_InitResult, err error) { return } + // TODO(spox): Update this to use sdk core interface (against server so manual map required) raw, err := b.callDynamicFunc( ctx, b.logger, @@ -179,67 +221,125 @@ func (b *Basis) Init() (result *vagrant_server.Job_InitResult, err error) { return } -func (b *Basis) LoadProject(ctx context.Context, popts ...ProjectOption) (p *Project, err error) { +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 +} + +func (b *Basis) LoadProject(popts ...ProjectOption) (p *Project, err error) { // Create our project p = &Project{ + ctx: b.ctx, basis: b, logger: b.logger.Named("project"), mappers: b.mappers, factories: b.factories, - machines: map[string]*Machine{}, + targets: map[string]*Target{}, UI: b.UI, - env: b.env, } - var opts options // Apply any options provided for _, opt := range popts { - opt(p, &opts) + 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 } // Ensure project directory is set if p.dir == nil { - return nil, fmt.Errorf("WithProjectDataDir must be specified") - } - - // Validate the configuration - if err = opts.Config.Validate(); err != nil { - return - } - - // Validate the labels - if errs := config.ValidateLabels(p.overrideLabels); len(errs) > 0 { - return nil, multierror.Append(nil, errs...) - } - - p.labels = opts.Config.Labels - - for _, mCfg := range opts.Config.Machines { - var d *datadir.Machine - if d, err = p.dir.Machine(mCfg.Name); err != nil { + if p.dir, err = b.dir.Project(p.project.Name); err != nil { return } + } - m := &Machine{ - name: mCfg.Name, - config: mCfg, - logger: p.logger.Named(mCfg.Name), - dir: d, - UI: terminal.ConsoleUI(ctx), + // Init server to make this project available + p.grpcServer = grpc.NewServer() + vagrant_plugin_sdk.RegisterProjectServiceServer(p.grpcServer, p) + + // Close down the local server when complete + p.Closer(func() error { + p.grpcServer.GracefulStop() + return nil + }) + + // Ensure any modifications to the project are persisted + p.Closer(func() error { return p.Save() }) + + return +} + +func (b *Basis) Closer(c func() error) { + b.closers = append(b.closers, c) +} + +func (b *Basis) Close() (err error) { + defer b.lock.Unlock() + b.lock.Lock() + + b.logger.Debug("closing basis", "basis", b.ResourceId()) + + // 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) } + } - p.machines[m.name] = m + // Call any closers that were registered locally + for _, c := range b.closers { + if cerr := c(); cerr != nil { + b.logger.Warn("error executing closer", "error", cerr) + err = multierror.Append(err, cerr) + } } return } -func (b *Basis) Close() error { - for _, c := range b.closers { - c() +// Saves the basis to the db +func (b *Basis) Save() (err error) { + b.logger.Debug("saving basis to db", "basis", b.ResourceId()) + _, err := b.Client().UpsertBasis(b.ctx, &vagrant_server.UpsertBasisRequest{ + Basis: b.basis}) + if err != nil { + b.logger.Trace("failed to save basis", "basis", b.ResourceId(), "error", err) } + return +} - return nil +// Saves the basis to the db as well as any projects that have been loaded +func (b *Basis) SaveFull() (err error) { + b.logger.Debug("performing full save", "basis", b.ResourceId()) + for _, p := range b.projects { + b.logger.Trace("saving project", "basis", b.ResourceId(), "project", p.ResourceId()) + if perr := p.SaveFull(); perr != nil { + b.logger.Trace("error while saving project", "project", p.ResourceId(), "error", err) + err = multierror.Append(err, perr) + } + } + if berr := b.Save(); berr != nil { + err = multierror.Append(err, berr) + } + return } func (b *Basis) Components(ctx context.Context) ([]*Component, error) { @@ -309,7 +409,7 @@ func (b *Basis) specializeComponent(c *Component) (cmp plugin.PluginMetadata, er if cmp, ok = c.Value.(plugin.PluginMetadata); !ok { return nil, fmt.Errorf("component does not support specialization") } - cmp.SetRequestMetadata("basis_resource_id", b.resourceid) + cmp.SetRequestMetadata("basis_resource_id", b.ResourceId()) cmp.SetRequestMetadata("vagrant_service_endpoint", b.client.ServerTarget()) return @@ -409,7 +509,6 @@ func (b *Basis) callDynamicFunc( // Make sure we have access to our context and logger and default args args = append(args, argmapper.Typed(ctx, log), - argmapper.Named("labels", &component.LabelSet{Labels: c.labels}), ) // Build the chain and call it @@ -437,18 +536,6 @@ func (b *Basis) callDynamicFunc( return raw, nil } -func (b *Basis) mergeLabels(ls ...map[string]string) map[string]string { - result := map[string]string{} - - // Merge order - mergeOrder := []map[string]string{result, b.labels} - mergeOrder = append(mergeOrder, ls...) - mergeOrder = append(mergeOrder, b.overrideLabels) - - // Merge them - return labelsMerge(mergeOrder...) -} - func (b *Basis) execHook(ctx context.Context, log hclog.Logger, h *config.Hook) error { return execHook(ctx, b, log, h) } @@ -458,61 +545,87 @@ func (b *Basis) doOperation(ctx context.Context, log hclog.Logger, op operation) } // BasisOption is used to set options for NewBasis. -type BasisOption func(*Basis) +type BasisOption func(*Basis) error // WithClient sets the API client to use. func WithClient(client *serverclient.VagrantClient) BasisOption { - return func(b *Basis) { + return func(b *Basis) (err error) { b.client = client + return } } // WithLogger sets the logger to use with the project. If this option // is not provided, a default logger will be used (`hclog.L()`). func WithLogger(log hclog.Logger) BasisOption { - return func(b *Basis) { b.logger = log } + return func(b *Basis) (err error) { + b.logger = log + return + } } // WithFactory sets a factory for a component type. If this isn't set for // any component type, then the builtin mapper will be used. func WithFactory(t component.Type, f *factory.Factory) BasisOption { - return func(b *Basis) { b.factories[t] = f } + return func(b *Basis) (err error) { + b.factories[t] = f + return + } } func WithBasisConfig(c *config.Config) BasisOption { - return func(b *Basis) { b.config = c } + return func(b *Basis) (err error) { + b.config = c + return + } } // WithComponents sets the factories for components. func WithComponents(fs map[component.Type]*factory.Factory) BasisOption { - return func(b *Basis) { b.factories = fs } + return func(b *Basis) (err error) { + b.factories = fs + return + } } // WithMappers adds the mappers to the list of mappers. func WithMappers(m ...*argmapper.Func) BasisOption { - return func(b *Basis) { b.mappers = append(b.mappers, m...) } + return func(b *Basis) (err error) { + b.mappers = append(b.mappers, m...) + return + } } // WithUI sets the UI to use. If this isn't set, a BasicUI is used. func WithUI(ui terminal.UI) BasisOption { - return func(b *Basis) { b.UI = ui } + return func(b *Basis) (err error) { + b.UI = ui + return + } } // WithJobInfo sets the base job info used for any executed operations. func WithJobInfo(info *component.JobInfo) BasisOption { - return func(b *Basis) { b.jobInfo = info } + return func(b *Basis) (err error) { + b.jobInfo = info + return + } } func WithBasisDataDir(dir *datadir.Basis) BasisOption { - return func(b *Basis) { b.dir = dir } + return func(b *Basis) (err error) { + b.dir = dir + return + } } -func WithBasisRef(r *vagrant_server.Ref_Basis) BasisOption { - return func(b *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 == "" { - result, err := b.client.UpsertBasis( + var result *vagrant_server.UpsertBasisResponse + result, err = b.client.UpsertBasis( context.Background(), &vagrant_server.UpsertBasisRequest{ Basis: &vagrant_server.Basis{ @@ -522,31 +635,50 @@ func WithBasisRef(r *vagrant_server.Ref_Basis) BasisOption { }, ) if err != nil { - panic("failed to upsert basis") // TODO(spox): don't panic + return } basis = result.Basis } else { - result, err := b.client.GetBasis( + var result *vagrant_server.GetBasisResponse + result, err = b.client.GetBasis( context.Background(), &vagrant_server.GetBasisRequest{ Basis: r, }, ) if err != nil { - panic("failed to retrieve basis") // TODO(spox): don't panic + return } basis = result.Basis } - b.name = basis.Name - b.resourceid = basis.ResourceId + b.basis = basis // if the datadir isn't set, do that now if b.dir == nil { - var err error b.dir, err = datadir.NewBasis(basis.Path) if err != nil { - panic("failed to setup basis datadir") // TODO(spox): don't panic + return } } + return + } +} + +func WithBasisResourceId(rid string) BasisOption { + return func(b *Basis) (err error) { + result, err := b.client.FindBasis(b.ctx, &vagrant_server.FindBasisRequest{ + Basis: &vagrant_server.Basis{ + ResourceId: rid, + }, + }) + if err != nil { + return + } + if !result.Found { + b.logger.Error("failed to locate basis during setup", "resource-id", rid) + return errors.New("requested basis is not found") + } + b.basis = result.Basis + return } } diff --git a/internal/core/operation.go b/internal/core/operation.go index 0eb5cb71f..925469cfd 100644 --- a/internal/core/operation.go +++ b/internal/core/operation.go @@ -24,7 +24,6 @@ type scope interface { JobInfo() *component.JobInfo Client() *serverclient.VagrantClient execHook(ctx context.Context, log hclog.Logger, h *config.Hook) (err error) - mergeLabels(...map[string]string) map[string]string } // operation is a private interface that we implement for "operations" such diff --git a/internal/core/project.go b/internal/core/project.go index 421a666ce..7fdba9353 100644 --- a/internal/core/project.go +++ b/internal/core/project.go @@ -2,19 +2,22 @@ package core import ( "context" - "io" + "errors" "strings" "sync" "github.com/golang/protobuf/proto" "github.com/hashicorp/go-argmapper" "github.com/hashicorp/go-hclog" + "github.com/hashicorp/go-multierror" + "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "github.com/hashicorp/vagrant-plugin-sdk/component" "github.com/hashicorp/vagrant-plugin-sdk/datadir" + "github.com/hashicorp/vagrant-plugin-sdk/proto/vagrant_plugin_sdk" "github.com/hashicorp/vagrant-plugin-sdk/terminal" "github.com/hashicorp/vagrant/internal/config" @@ -29,122 +32,100 @@ 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 config *config.Project logger hclog.Logger - machines map[string]*Machine + targets map[string]*Target factories map[component.Type]*factory.Factory dir *datadir.Project mappers []*argmapper.Func - env *Environment - // name is the name of the project - name string - - // path is the location of the project - path string - - // resourceid is the unique identifier of the project - resourceid string - - // labels is the list of labels that are assigned to this project. - labels map[string]string + grpcServer *grpc.Server // jobInfo is the base job info for executed functions. jobInfo *component.JobInfo - // This lock only needs to be held currently to protect localClosers. + // This lock only needs to be held currently to protect closers. lock sync.Mutex // The below are resources we need to close when Close is called, if non-nil - localClosers []io.Closer + closers []func() error // 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 - - // overrideLabels are the labels specified via the CLI to override - // all other conflicting keys. - overrideLabels map[string]string } func (p *Project) Ui() terminal.UI { return p.UI } +func (p *Project) Name() string { + return p.project.Name +} + +func (p *Project) ResourceId() string { + return p.project.ResourceId +} + func (p *Project) JobInfo() *component.JobInfo { return p.jobInfo } -func (p *Project) Environment() *Environment { - return p.env +func (p *Project) Target(nameOrId string) *Target { + if t, ok := p.targets[nameOrId]; ok { + return t + } + for _, t := range p.targets { + if t.target.ResourceId == nameOrId { + return t + } + } + return nil } -func (p *Project) MachineFromRef(r *vagrant_server.Ref_Machine) (*Machine, error) { - var machine *vagrant_server.Machine - if r.ResourceId != "" { - result, err := p.Client().GetMachine( - context.Background(), - &vagrant_server.GetMachineRequest{ - Project: p.Ref().(*vagrant_server.Ref_Project), - Machine: r, - }, - ) - if err != nil { - return nil, err - } - machine = result.Machine - } else { - result, err := p.Client().UpsertMachine( - context.Background(), - &vagrant_server.UpsertMachineRequest{ - Project: p.Ref().(*vagrant_server.Ref_Project), - Machine: &vagrant_server.Machine{ - Name: r.Name, - Project: p.Ref().(*vagrant_server.Ref_Project), - }, - }, - ) - if err != nil { - return nil, err - } - machine = result.Machine +func (p *Project) LoadTarget(topts ...TargetOption) (t *Target, err error) { + // Create our target + t = &Target{ + ctx: p.ctx, + project: p, + logger: p.logger.Named("target"), + UI: p.UI, } - mdir, err := p.dir.Machine(machine.Name) + + // 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 - } - m := &Machine{ - name: machine.Name, - resourceid: machine.ResourceId, - project: p, - logger: p.logger.Named(machine.Name), - dir: mdir, - UI: p.UI, + return } - return m, nil -} - -// App initializes and returns the machine with the given name. -func (p *Project) Machine(name string) (*Machine, error) { - m, ok := p.machines[name] - if !ok { - d, err := p.dir.Machine(name) - if err != nil { - return nil, err - } - m = &Machine{ - name: name, - project: p, - logger: p.logger.Named(name), - dir: d, - UI: p.UI, - } - p.machines[name] = m + // If the machine is already loaded, return that + if target, ok := p.targets[t.Name()]; ok { + return target, nil } - return p.machines[name], nil + + // Init server to make this target available + t.grpcServer = grpc.NewServer() + vagrant_plugin_sdk.RegisterTargetServiceServer(t.grpcServer, t) + + // Close down the local server when complete + t.Closer(func() error { + t.grpcServer.GracefulStop() + return nil + }) + + // Ensure any modifications to the target are persisted + t.Closer(func() error { return t.Save() }) + + return } // Client returns the API client for the backend server. @@ -154,10 +135,10 @@ func (p *Project) Client() *serverclient.VagrantClient { // Ref returns the project ref for API calls. func (p *Project) Ref() interface{} { - return &vagrant_server.Ref_Project{ - ResourceId: p.resourceid, - Name: p.name, - Basis: p.basis.Ref().(*vagrant_server.Ref_Basis), + return &vagrant_plugin_sdk.Ref_Project{ + ResourceId: p.project.ResourceId, + Name: p.project.Name, + Basis: p.project.Basis, } } @@ -189,14 +170,6 @@ func (p *Project) Components(ctx context.Context) (results []*Component, err err return results, nil } -func (p *Project) specializeComponent(c *Component) (cmp plugin.PluginMetadata, err error) { - if cmp, err = p.basis.specializeComponent(c); err != nil { - return - } - cmp.SetRequestMetadata("project_resource_id", p.resourceid) - return -} - func (p *Project) Run(ctx context.Context, task *vagrant_server.Task) (err error) { p.logger.Debug("running new task", "project", p, "task", task) @@ -217,8 +190,6 @@ func (p *Project) Run(ctx context.Context, task *vagrant_server.Task) (err error cmd, cmd.Value.(component.Command).ExecuteFunc(strings.Split(task.CommandName, " ")), argmapper.Typed(task.CliArgs), - // TODO: add extra args here - argmapper.Typed(p.env), ) if err != nil || result == nil || result.(int64) != 0 { p.logger.Error("failed to execute command", "type", component.CommandType, "name", task.Component.Name, "result", result, "error", err) @@ -228,31 +199,64 @@ func (p *Project) Run(ctx context.Context, task *vagrant_server.Task) (err error return } +func (p *Project) Closer(c func() error) { + p.closers = append(p.closers, c) +} + // 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() error { - p.lock.Lock() +func (p *Project) Close() (err error) { defer p.lock.Unlock() + p.lock.Lock() p.logger.Debug("closing project", "project", p) - // Stop all our machines (not sure what this actually affects) - for name, m := range p.machines { - p.logger.Trace("closing machine", "machine", name) - if err := m.Close(); err != nil { - p.logger.Warn("error closing machine", "err", err) + // close all the loaded targets + for name, m := range p.targets { + p.logger.Trace("closing target", "target", name) + if cerr := m.Close(); cerr != nil { + p.logger.Warn("error closing target", "target", name, + "err", cerr) + err = multierror.Append(err, cerr) } } - // If we're running in local mode, close our local resources we started - for _, c := range p.localClosers { - if err := c.Close(); err != nil { - return err + for _, f := range p.closers { + if cerr := f(); cerr != nil { + p.logger.Warn("error executing closer", "error", cerr) + err = multierror.Append(err, cerr) } } - p.localClosers = nil + // Remove this project from built project list in basis + delete(p.basis.projects, p.Name()) + return +} - return nil +// Saves the project to the db +func (p *Project) Save() (err error) { + p.logger.Trace("saving project to db", "project", p.ResourceId()) + _, err := p.Client().UpsertProject(p.ctx, &vagrant_server.UpsertProjectRequest{ + Project: p.project}) + if err != nil { + p.logger.Trace("failed to save project", "project", p.ResourceId()) + } + 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.ResourceId()) + for _, t := range p.targets { + p.logger.Trace("saving target", "project", p.ResourceId(), "target", t.ResourceId()) + if terr := t.Save(); terr != nil { + p.logger.Trace("error while saving target", "target", t.ResourceId(), "error", err) + err = multierror.Append(err, terr) + } + } + if perr := p.Save(); perr != nil { + err = multierror.Append(err, perr) + } + return } func (p *Project) callDynamicFunc( @@ -280,21 +284,12 @@ func (p *Project) callDynamicFunc( return p.basis.callDynamicFunc(ctx, log, result, c, f, args...) } -// mergeLabels merges the set of labels given. This will set the project -// labels as a base automatically and then merge ls in order. -func (p *Project) mergeLabels(ls ...map[string]string) map[string]string { - result := map[string]string{} - - // Set our builtin labels - // result["vagrant/workspace"] = p.workspace - - // Merge order - mergeOrder := []map[string]string{result, p.labels} - mergeOrder = append(mergeOrder, ls...) - mergeOrder = append(mergeOrder, p.overrideLabels) - - // Merge them - return labelsMerge(mergeOrder...) +func (p *Project) specializeComponent(c *Component) (cmp plugin.PluginMetadata, err error) { + if cmp, err = p.basis.specializeComponent(c); err != nil { + return + } + cmp.SetRequestMetadata("project_resource_id", p.ResourceId()) + return } func (p *Project) execHook(ctx context.Context, log hclog.Logger, h *config.Hook) error { @@ -314,36 +309,90 @@ type options struct { } // ProjectOption is used to set options for LoadProject -type ProjectOption func(*Project, *options) - -// WithConfig uses the given project configuration for initializing the -// Project. This configuration must be validated already prior to using this -// option. -func WithConfig(c *config.Project) ProjectOption { - return func(p *Project, opts *options) { - opts.Config = c - } -} +type ProjectOption func(*Project) error func WithBasis(b *Basis) ProjectOption { - return func(p *Project, opts *options) { + return func(p *Project) (err error) { p.basis = b + return } } func WithProjectDataDir(dir *datadir.Project) ProjectOption { - return func(p *Project, opts *options) { + return func(p *Project) (err error) { p.dir = dir + return } } -func WithProjectRef(r *vagrant_server.Ref_Project) ProjectOption { - return func(p *Project, opts *options) { +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 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.Found { + 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.basis.projects[p.project.Name] = p + + return + } +} + +func WithProjectRef(r *vagrant_plugin_sdk.Ref_Project) ProjectOption { + return func(p *Project) (err error) { + // Basis must be set before we continue + if p.basis == nil { + return errors.New("basis must be set before loading project") + } + var project *vagrant_server.Project - // if we don't have a resource ID we need to upsert - if r.ResourceId == "" { - result, err := p.Client().UpsertProject( - context.Background(), + // 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 + } + result, err := p.Client().FindProject(p.ctx, + &vagrant_server.FindProjectRequest{ + Project: &vagrant_server.Project{ + Name: r.Name, + Path: r.Path, + }, + }, + ) + if err != nil { + return err + } + if result.Found { + project = result.Project + } else { + var result *vagrant_server.UpsertProjectResponse + result, err = p.Client().UpsertProject(p.ctx, &vagrant_server.UpsertProjectRequest{ Project: &vagrant_server.Project{ Name: r.Name, @@ -353,31 +402,20 @@ func WithProjectRef(r *vagrant_server.Ref_Project) ProjectOption { }, ) if err != nil { - panic("failed to upsert project") // TODO(spox): don't panic - } - project = result.Project - } else { - result, err := p.Client().GetProject( - context.Background(), - &vagrant_server.GetProjectRequest{ - Project: r, - }, - ) - if err != nil { - panic("failed to retrieve project") // TODO(spox): don't panic + return } project = result.Project } - p.name = project.Name - p.resourceid = project.ResourceId - p.path = project.Path - if p.dir == nil { - var err error - p.dir, err = datadir.NewProject(p.path + "/.vagrant") - if err != nil { - panic("failed to create project data dir") // TODO(spox): don't panic - } + // Before we init, validate basis is consistent + if 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") } + p.project = project + // Finally set the project into the basis + p.basis.projects[p.Name()] = p + return } } diff --git a/internal/core/target.go b/internal/core/target.go index 49e66ae7a..4fb24b55b 100644 --- a/internal/core/target.go +++ b/internal/core/target.go @@ -2,36 +2,40 @@ package core import ( "context" + "errors" "strings" + "sync" "github.com/golang/protobuf/proto" - "github.com/hashicorp/go-argmapper" "github.com/hashicorp/go-hclog" + "github.com/hashicorp/go-multierror" "github.com/hashicorp/vagrant/internal/config" "github.com/hashicorp/vagrant/internal/plugin" "github.com/hashicorp/vagrant/internal/serverclient" + "google.golang.org/grpc" "github.com/hashicorp/vagrant-plugin-sdk/component" "github.com/hashicorp/vagrant-plugin-sdk/datadir" "github.com/hashicorp/vagrant-plugin-sdk/proto/vagrant_plugin_sdk" "github.com/hashicorp/vagrant-plugin-sdk/terminal" + "github.com/hashicorp/vagrant/internal/server/proto/vagrant_server" ) type Target struct { - name string - resourceid string - project *Project - logger hclog.Logger - config *config.Target - dir *datadir.Target + ctx context.Context + target *vagrant_server.Target + project *Project + logger hclog.Logger + config *config.Target + dir *datadir.Target - labels map[string]string - overrideLabels map[string]string - - jobInfo *component.JobInfo - UI terminal.UI + grpcServer *grpc.Server + lock sync.Mutex + jobInfo *component.JobInfo + closers []func() error + UI terminal.UI } func (t *Target) Ui() terminal.UI { @@ -40,53 +44,79 @@ func (t *Target) Ui() terminal.UI { func (t *Target) Ref() interface{} { return &vagrant_plugin_sdk.Ref_Target{ - ResourceId: m.resourceid, - Name: m.name, - Project: m.project.Ref().(*vagrant_server.Ref_Project), + ResourceId: t.target.ResourceId, + Project: t.target.Project, } } -func (m *Machine) JobInfo() *component.JobInfo { - return m.jobInfo +func (t *Target) Name() string { + return t.target.Name } -func (m *Machine) Client() *serverclient.VagrantClient { - return m.project.basis.client +func (t *Target) ResourceId() string { + return t.target.ResourceId } -func (m *Machine) Close() (err error) { - return +func (t *Target) JobInfo() *component.JobInfo { + return t.jobInfo } -func (m *Machine) specializeComponent(c *Component) (cmp plugin.PluginMetadata, err error) { - if cmp, err = m.project.specializeComponent(c); err != nil { - return +func (t *Target) Client() *serverclient.VagrantClient { + return t.project.basis.client +} + +func (t *Target) Closer(c func() error) { + t.closers = append(t.closers, c) +} + +func (t *Target) Close() (err error) { + defer t.lock.Unlock() + t.lock.Lock() + + t.logger.Debug("closing target", "target", t) + + for _, c := range t.closers { + if cerr := c(); cerr != nil { + t.logger.Warn("error executing closer", "error", cerr) + err = multierror.Append(err, cerr) + } } - cmp.SetRequestMetadata("machine_resource_id", m.resourceid) + // Remove this target from built target list in project + delete(t.project.targets, t.target.Name) return } -func (m *Machine) Run(ctx context.Context, task *vagrant_server.Task) (err error) { - m.logger.Debug("running new task", "machine", m, "task", task) +func (t *Target) Save() (err error) { + t.logger.Debug("saving target to db", "target", t.ResourceId()) + _, err := t.Client().UpsertTarget(t.ctx, &vagrant_server.UpsertTargetRequest{ + Target: t.target}) + if err != nil { + t.logger.Trace("failed to save target", "target", t.ResourceId(), "error", err) + } + return +} - cmd, err := m.project.basis.component( +func (t *Target) Run(ctx context.Context, task *vagrant_server.Task) (err error) { + t.logger.Debug("running new task", "target", t, "task", task) + + cmd, err := t.project.basis.component( ctx, component.CommandType, task.Component.Name) if err != nil { - m.logger.Error("failed to build requested component", "type", component.CommandType, + t.logger.Error("failed to build requested component", "type", component.CommandType, "name", task.Component.Name, "error", err) - return err + return } - if _, err = m.specializeComponent(cmd); err != nil { - m.logger.Error("failed to specialize component", "type", component.CommandType, + if _, err = t.specializeComponent(cmd); err != nil { + t.logger.Error("failed to specialize component", "type", component.CommandType, "name", task.Component.Name, "error", err) - return err + return } - result, err := m.callDynamicFunc( + result, err := t.callDynamicFunc( ctx, - m.logger, + t.logger, (interface{})(nil), cmd, cmd.Value.(component.Command).ExecuteFunc(strings.Split(task.CommandName, " ")), @@ -94,15 +124,14 @@ func (m *Machine) Run(ctx context.Context, task *vagrant_server.Task) (err error ) if err != nil || result == nil || result.(int64) != 0 { - m.logger.Error("failed to execute command", "type", component.CommandType, + t.logger.Error("failed to execute command", "type", component.CommandType, "name", task.Component.Name, "error", err) - return err } return } -func (m *Machine) callDynamicFunc( +func (t *Target) callDynamicFunc( ctx context.Context, log hclog.Logger, result interface{}, // expected result type @@ -113,37 +142,109 @@ func (m *Machine) callDynamicFunc( // Be sure that the status is closed after every operation so we don't leak // weird output outside the normal execution. - defer m.UI.Status().Close() + defer t.UI.Status().Close() args = append(args, argmapper.Typed( - m.jobInfo, - m.dir, - m.UI, + t.jobInfo, + t.dir, + t.UI, ), ) - return m.project.callDynamicFunc(ctx, log, result, c, f, args...) + return t.project.callDynamicFunc(ctx, log, result, c, f, args...) } -func (m *Machine) mergeLabels(ls ...map[string]string) map[string]string { - result := map[string]string{} - - // Merge order - mergeOrder := []map[string]string{result, m.labels} - mergeOrder = append(mergeOrder, ls...) - mergeOrder = append(mergeOrder, m.overrideLabels) - - // Merge them - return labelsMerge(mergeOrder...) +func (t *Target) specializeComponent(c *Component) (cmp plugin.PluginMetadata, err error) { + if cmp, err = t.project.specializeComponent(c); err != nil { + return + } + cmp.SetRequestMetadata("target_resource_id", t.target.ResourceId) + return } -func (m *Machine) execHook(ctx context.Context, log hclog.Logger, h *config.Hook) error { - return execHook(ctx, m, log, h) +func (t *Target) execHook(ctx context.Context, log hclog.Logger, h *config.Hook) error { + return execHook(ctx, t, log, h) } -func (m *Machine) doOperation(ctx context.Context, log hclog.Logger, op operation) (interface{}, proto.Message, error) { - return doOperation(ctx, log, m, op) +func (t *Target) doOperation(ctx context.Context, log hclog.Logger, op operation) (interface{}, proto.Message, error) { + return doOperation(ctx, log, t, op) } -var _ *Machine = (*Machine)(nil) +type TargetOption func(*Target) error + +func WithTargetName(name string) TargetOption { + return func(t *Target) (err error) { + if ex := t.project.Target(name); ex != nil { + t.target = ex.target + return + } + for _, target := range t.project.targets { + if target.Name() != name { + continue + } + var result *vagrant_server.GetTargetResponse + result, err = t.Client().GetTarget(t.ctx, + &vagrant_server.GetTargetRequest{Target: target.Ref().(*vagrant_plugin_sdk.Ref_Target)}) + if err != nil { + return + } + t.target = result.Target + t.project.targets[t.target.Name] = t + return + } + return errors.New("target is not registered in project") + } +} + +func WithTargetRef(r *vagrant_plugin_sdk.Ref_Target) TargetOption { + return func(t *Target) (err error) { + // Project must be set before we continue + if t.project == nil { + return errors.New("project must be set before loading target") + } + + var target *vagrant_server.Target + if ex := t.project.Target(r.Name); ex != nil { + t.target = ex.target + return + } + result, err := t.Client().FindTarget(t.ctx, + &vagrant_server.FindTargetRequest{ + Target: &vagrant_server.Target{ + Name: r.Name, + }, + }, + ) + if err != nil { + return err + } + if result.Found { + target = result.Target + } else { + var result *vagrant_server.UpsertTargetResponse + result, err = t.Client().UpsertTarget(t.ctx, + &vagrant_server.UpsertTargetRequest{ + Target: &vagrant_server.Target{ + Name: r.Name, + Project: r.Project, + }, + }, + ) + if err != nil { + return + } + target = result.Target + } + if target.Project.ResourceId != r.Project.ResourceId { + t.logger.Error("invalid project for target", "request-project", r.Project, + "target-project", target.Project) + return errors.New("target project configuration is invalid") + } + t.target = target + t.project.targets[t.Name()] = t + return + } +} + +var _ *Target = (*Target)(nil)