It is not guaranteed which project is ever being used. So whenever some project property is queried make sure to refresh the project by getting the latest one from the database.
690 lines
16 KiB
Go
690 lines
16 KiB
Go
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"
|
|
"google.golang.org/grpc/status"
|
|
|
|
"github.com/hashicorp/vagrant-plugin-sdk/component"
|
|
"github.com/hashicorp/vagrant-plugin-sdk/core"
|
|
"github.com/hashicorp/vagrant-plugin-sdk/datadir"
|
|
"github.com/hashicorp/vagrant-plugin-sdk/helper/path"
|
|
"github.com/hashicorp/vagrant-plugin-sdk/helper/paths"
|
|
"github.com/hashicorp/vagrant-plugin-sdk/internal-shared/cacher"
|
|
"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/server/proto/vagrant_server"
|
|
"github.com/hashicorp/vagrant/internal/serverclient"
|
|
)
|
|
|
|
// Project represents a project with one or more applications.
|
|
//
|
|
// 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
|
|
targets map[string]*Target
|
|
dir *datadir.Project
|
|
mappers []*argmapper.Func
|
|
|
|
// 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
|
|
|
|
// The below are resources we need to close when Close is called, if non-nil
|
|
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
|
|
}
|
|
|
|
// ActiveTargets implements core.Project
|
|
func (p *Project) ActiveTargets() (activeTargets []core.Target, err error) {
|
|
targets, err := p.Targets()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
activeTargets = []core.Target{}
|
|
for _, t := range targets {
|
|
st, err := t.State()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if st == core.CREATED {
|
|
activeTargets = append(activeTargets, t)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// Boxes implements core.Project
|
|
func (p *Project) Boxes() (bc core.BoxCollection, err error) {
|
|
return p.basis.Boxes()
|
|
}
|
|
|
|
// Config implements core.Project
|
|
func (p *Project) Config() (*vagrant_plugin_sdk.Vagrantfile_Vagrantfile, error) {
|
|
p.refreshProject()
|
|
return p.project.Configuration, nil
|
|
}
|
|
|
|
// CWD implements core.Project
|
|
func (p *Project) CWD() (path path.Path, err error) {
|
|
return paths.VagrantCwd()
|
|
}
|
|
|
|
// DataDir implements core.Project
|
|
func (p *Project) DataDir() (*datadir.Project, error) {
|
|
return p.dir, nil
|
|
}
|
|
|
|
// DefaultPrivateKey implements core.Project
|
|
func (p *Project) DefaultPrivateKey() (path path.Path, err error) {
|
|
return p.basis.DefaultPrivateKey()
|
|
}
|
|
|
|
// DefaultProvider implements core.Project
|
|
func (p *Project) DefaultProvider() (name string, err error) {
|
|
// TODO: This needs to implement the default provider algorithm
|
|
// from https://www.vagrantup.com/docs/providers/basic_usage.html#default-provider
|
|
return "virtualbox", nil
|
|
}
|
|
|
|
// Home implements core.Project
|
|
func (p *Project) Home() (dir path.Path, err error) {
|
|
p.refreshProject()
|
|
return path.NewPath(p.project.Path), nil
|
|
}
|
|
|
|
// Host implements core.Project
|
|
func (p *Project) Host() (host core.Host, err error) {
|
|
return p.basis.Host()
|
|
}
|
|
|
|
// LocalData implements core.Project
|
|
func (p *Project) LocalData() (d path.Path, err error) {
|
|
return p.dir.DataDir(), nil
|
|
}
|
|
|
|
// PrimaryTargetName implements core.Project
|
|
func (p *Project) PrimaryTargetName() (name string, err error) {
|
|
// TODO: This needs the Vagrantfile service to be implemented
|
|
return
|
|
}
|
|
|
|
// Resource implements core.Project
|
|
func (p *Project) ResourceId() (string, error) {
|
|
p.refreshProject()
|
|
return p.project.ResourceId, nil
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Target implements core.Project
|
|
func (p *Project) Target(nameOrId string) (core.Target, error) {
|
|
if t, ok := p.targets[nameOrId]; ok {
|
|
return t, nil
|
|
}
|
|
// Check for name or id
|
|
for _, t := range p.targets {
|
|
if t.target.Name == nameOrId {
|
|
return t, nil
|
|
}
|
|
if t.target.ResourceId == nameOrId {
|
|
return t, nil
|
|
}
|
|
}
|
|
// Finally try loading it
|
|
return p.LoadTarget(
|
|
WithTargetRef(
|
|
&vagrant_plugin_sdk.Ref_Target{
|
|
Project: p.Ref().(*vagrant_plugin_sdk.Ref_Project),
|
|
Name: nameOrId,
|
|
ResourceId: nameOrId,
|
|
},
|
|
),
|
|
)
|
|
}
|
|
|
|
// TargetIds implements core.Project
|
|
func (p *Project) TargetIds() ([]string, error) {
|
|
p.refreshProject()
|
|
var ids []string
|
|
for _, t := range p.project.Targets {
|
|
ids = append(ids, t.ResourceId)
|
|
}
|
|
return ids, nil
|
|
}
|
|
|
|
// TargetIndex implements core.Project
|
|
func (p *Project) TargetIndex() (index core.TargetIndex, err error) {
|
|
return p.basis.TargetIndex()
|
|
}
|
|
|
|
// TargetNames implements core.Project
|
|
func (p *Project) TargetNames() ([]string, error) {
|
|
p.refreshProject()
|
|
var names []string
|
|
for _, t := range p.project.Targets {
|
|
names = append(names, t.Name)
|
|
}
|
|
return names, nil
|
|
}
|
|
|
|
// Tmp implements core.Project
|
|
func (p *Project) Tmp() (path path.Path, err error) {
|
|
return p.dir.TempDir(), nil
|
|
}
|
|
|
|
// UI implements core.Project
|
|
func (p *Project) UI() (terminal.UI, error) {
|
|
return p.ui, nil
|
|
}
|
|
|
|
// VagrantfileName implements core.Project
|
|
func (p *Project) VagrantfileName() (name string, err error) {
|
|
p.refreshProject()
|
|
fullPath := path.NewPath(p.project.Configuration.Path)
|
|
return fullPath.Base().String(), nil
|
|
}
|
|
|
|
// VagrantfilePath implements core.Project
|
|
func (p *Project) VagrantfilePath() (pp path.Path, err error) {
|
|
p.refreshProject()
|
|
pp = path.NewPath(p.project.Configuration.Path).Parent()
|
|
return
|
|
}
|
|
|
|
// Targets
|
|
func (p *Project) Targets() ([]core.Target, error) {
|
|
p.refreshProject()
|
|
var targets []core.Target
|
|
for _, ref := range p.project.Targets {
|
|
t, err := p.LoadTarget(WithTargetRef(ref))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
targets = append(targets, t)
|
|
}
|
|
return targets, nil
|
|
}
|
|
|
|
// Custom name defined for this project
|
|
func (p *Project) Name() string {
|
|
p.refreshProject()
|
|
return p.project.Name
|
|
}
|
|
|
|
// Returns the job info if currently set
|
|
func (p *Project) JobInfo() *component.JobInfo {
|
|
return p.jobInfo
|
|
}
|
|
|
|
// Load a project within the current basis. If the project is not found, it
|
|
// will be created.
|
|
func (p *Project) LoadTarget(topts ...TargetOption) (t *Target, err error) {
|
|
p.m.Lock()
|
|
defer p.m.Unlock()
|
|
|
|
// Create our target
|
|
t = &Target{
|
|
cache: cacher.New(),
|
|
ctx: p.ctx,
|
|
project: p,
|
|
logger: p.logger,
|
|
ui: p.ui,
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
if t.dir == nil {
|
|
if t.dir, err = p.dir.Target(t.target.Name); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// If the machine is already loaded, return that
|
|
if target, ok := p.targets[t.target.ResourceId]; ok {
|
|
return target, nil
|
|
}
|
|
|
|
p.targets[t.target.ResourceId] = t
|
|
|
|
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() })
|
|
|
|
return
|
|
}
|
|
|
|
// Client returns the API client for the backend server.
|
|
func (p *Project) Client() *serverclient.VagrantClient {
|
|
return p.basis.client
|
|
}
|
|
|
|
// Ref returns the project ref for API calls.
|
|
func (p *Project) Ref() interface{} {
|
|
p.refreshProject()
|
|
return &vagrant_plugin_sdk.Ref_Project{
|
|
ResourceId: p.project.ResourceId,
|
|
Name: p.project.Name,
|
|
Basis: p.project.Basis,
|
|
}
|
|
}
|
|
|
|
func (p *Project) Run(ctx context.Context, task *vagrant_server.Task) (err error) {
|
|
p.logger.Debug("running new task",
|
|
"project", p,
|
|
"task", task)
|
|
|
|
// Intialize targets
|
|
if err = p.InitTargets(); err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd, err := p.basis.component(
|
|
ctx, component.CommandType, task.Component.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fn := cmd.Value.(component.Command).ExecuteFunc(
|
|
strings.Split(task.CommandName, " "))
|
|
result, err := p.callDynamicFunc(ctx, p.logger, fn, (*int32)(nil),
|
|
argmapper.Typed(ctx, task.CliArgs, p.jobInfo),
|
|
argmapper.ConverterFunc(cmd.mappers...),
|
|
)
|
|
|
|
p.logger.Warn("completed running command from project", "result", result)
|
|
|
|
if err != nil || result == nil || result.(int32) != 0 {
|
|
p.logger.Error("failed to execute command",
|
|
"type", component.CommandType,
|
|
"name", task.Component.Name,
|
|
"result", result,
|
|
"error", err,
|
|
)
|
|
|
|
cmdErr := &runError{}
|
|
if err != nil {
|
|
cmdErr.err = err
|
|
if st, ok := status.FromError(err); ok {
|
|
cmdErr.status = st.Proto()
|
|
}
|
|
}
|
|
if result != nil {
|
|
cmdErr.exitCode = result.(int32)
|
|
}
|
|
|
|
return cmdErr
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (p *Project) seed(fn func(*core.Seeds)) {
|
|
p.basis.seed(
|
|
func(s *core.Seeds) {
|
|
s.AddNamed("project", p)
|
|
s.AddNamed("project_ui", p.ui)
|
|
s.AddTyped(p)
|
|
if fn != nil {
|
|
fn(s)
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
// Register functions to be called when closing this project
|
|
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() (err error) {
|
|
p.logger.Debug("closing project",
|
|
"project", p)
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
|
|
for _, f := range p.closers {
|
|
if cerr := f(); cerr != nil {
|
|
p.logger.Warn("error executing closer",
|
|
"error", cerr)
|
|
|
|
err = multierror.Append(err, cerr)
|
|
}
|
|
}
|
|
// Remove this project from built project list in basis
|
|
delete(p.basis.projects, p.Name())
|
|
return
|
|
}
|
|
|
|
// Saves the project to the db
|
|
func (p *Project) Save() (err error) {
|
|
p.m.Lock()
|
|
defer p.m.Unlock()
|
|
|
|
p.logger.Trace("saving project to db",
|
|
"project", p.project.ResourceId)
|
|
|
|
result, err := p.Client().UpsertProject(p.ctx,
|
|
&vagrant_server.UpsertProjectRequest{
|
|
Project: p.project,
|
|
},
|
|
)
|
|
if err != nil {
|
|
p.logger.Trace("failed to save project",
|
|
"project", p.project.ResourceId)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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()
|
|
|
|
p.logger.Trace("initializing targets defined within project",
|
|
"project", p.Name())
|
|
|
|
if p.project.Configuration == nil || p.project.Configuration.MachineConfigs == nil {
|
|
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("known targets within project",
|
|
"project", p.Name(),
|
|
"targets", existingTargets,
|
|
)
|
|
|
|
updated := false
|
|
for _, t := range p.project.Configuration.MachineConfigs {
|
|
if t == nil {
|
|
continue
|
|
}
|
|
_, err = p.Client().UpsertTarget(p.ctx,
|
|
&vagrant_server.UpsertTargetRequest{
|
|
Target: &vagrant_server.Target{
|
|
Name: t.Name,
|
|
Project: p.Ref().(*vagrant_plugin_sdk.Ref_Project),
|
|
Configuration: t,
|
|
},
|
|
},
|
|
)
|
|
if err != nil {
|
|
p.logger.Error("failed to initialize target with project",
|
|
"project", p.Name(),
|
|
"target", t.Name,
|
|
"error", err,
|
|
)
|
|
|
|
return
|
|
}
|
|
updated = true
|
|
}
|
|
|
|
if !updated {
|
|
return
|
|
}
|
|
|
|
err = p.refreshProject()
|
|
return
|
|
}
|
|
|
|
// Get's the latest project from the DB and caches is
|
|
func (p *Project) refreshProject() (err error) {
|
|
result, err := p.Client().FindProject(p.ctx,
|
|
&vagrant_server.FindProjectRequest{
|
|
Project: &vagrant_server.Project{
|
|
ResourceId: p.project.ResourceId,
|
|
},
|
|
},
|
|
)
|
|
if err != nil {
|
|
p.logger.Error("failed to refresh project data",
|
|
"project", p.Name(),
|
|
"error", err,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
p.project = result.Project
|
|
return
|
|
}
|
|
|
|
// Calls the function provided and converts the
|
|
// result to an expected type. If no type conversion
|
|
// is required, a `false` value for the expectedType
|
|
// will return the raw interface return value.
|
|
//
|
|
// By default, the project is added as a typed argument
|
|
// and the project and project UI are both added as a
|
|
// named arguments. Execution is passed up to the basis
|
|
// level so it can set arguments as well and actually
|
|
// execute the function.
|
|
func (p *Project) callDynamicFunc(
|
|
ctx context.Context, // context for function execution
|
|
log hclog.Logger, // logger to provide function execution
|
|
f interface{}, // function to call
|
|
expectedType interface{}, // nil pointer of expected return type
|
|
args ...argmapper.Arg, // list of argmapper arguments
|
|
) (interface{}, error) {
|
|
// ensure our UI status is closed after every call in case it is used
|
|
defer p.ui.Status().Close()
|
|
|
|
return p.basis.callDynamicFunc(ctx, log, f, expectedType, args...)
|
|
}
|
|
|
|
func (p *Project) execHook(
|
|
ctx context.Context,
|
|
log hclog.Logger,
|
|
h *config.Hook,
|
|
) error {
|
|
return execHook(ctx, p, log, h)
|
|
}
|
|
|
|
func (p *Project) doOperation(
|
|
ctx context.Context,
|
|
log hclog.Logger,
|
|
op operation,
|
|
) (interface{}, proto.Message, error) {
|
|
return doOperation(ctx, log, p, op)
|
|
}
|
|
|
|
// ProjectOption is used to set options for LoadProject
|
|
type ProjectOption func(*Project) error
|
|
|
|
func WithBasis(b *Basis) ProjectOption {
|
|
return func(p *Project) (err error) {
|
|
p.basis = b
|
|
return
|
|
}
|
|
}
|
|
|
|
func WithProjectDataDir(dir *datadir.Project) ProjectOption {
|
|
return func(p *Project) (err error) {
|
|
p.dir = dir
|
|
return
|
|
}
|
|
}
|
|
|
|
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 == 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
|
|
|
|
return
|
|
}
|
|
}
|
|
|
|
// WithBasisRef is used to load or initialize the project
|
|
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
|
|
// 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{
|
|
Basis: r.Basis,
|
|
Name: r.Name,
|
|
Path: r.Path,
|
|
},
|
|
},
|
|
)
|
|
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
|
|
}
|
|
|
|
// 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
|
|
|
|
return
|
|
}
|
|
}
|
|
|
|
var _ core.Project = (*Project)(nil)
|