This should be updated once the box collection and provider are implemented in order to select the right box from the box collection.
672 lines
16 KiB
Go
672 lines
16 KiB
Go
package core
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"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/protobuf/types/known/anypb"
|
|
|
|
"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/proto/vagrant_plugin_sdk"
|
|
"github.com/hashicorp/vagrant-plugin-sdk/terminal"
|
|
|
|
"github.com/hashicorp/vagrant/internal/config"
|
|
"github.com/hashicorp/vagrant/internal/factory"
|
|
"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
|
|
config *config.Project
|
|
logger hclog.Logger
|
|
targets map[string]*Target
|
|
factories map[component.Type]*factory.Factory
|
|
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
|
|
}
|
|
|
|
// UI implements core.Project
|
|
func (p *Project) UI() (terminal.UI, error) {
|
|
return p.ui, nil
|
|
}
|
|
|
|
// CWD implements core.Project
|
|
func (p *Project) CWD() (path string, err error) {
|
|
cwd, ok := os.LookupEnv("VAGRANT_CWD")
|
|
if ok {
|
|
if _, err := os.Stat(cwd); !os.IsNotExist(err) {
|
|
// cwd exists
|
|
return cwd, nil
|
|
} else {
|
|
return "", errors.New("VAGRANT_CWD set to path that does not exist")
|
|
}
|
|
}
|
|
return os.Getwd()
|
|
}
|
|
|
|
// DataDir implements core.Project
|
|
func (p *Project) DataDir() (*datadir.Project, error) {
|
|
return p.dir, nil
|
|
}
|
|
|
|
// VagrantfileName implements core.Project
|
|
func (p *Project) VagrantfileName() (name string, err error) {
|
|
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) {
|
|
pp = path.NewPath(p.project.Configuration.Path).Parent()
|
|
return
|
|
}
|
|
|
|
// Home implements core.Project
|
|
func (p *Project) Home() (path string, err error) {
|
|
return p.project.Path, nil
|
|
}
|
|
|
|
// LocalData implements core.Project
|
|
func (p *Project) LocalData() (path string, err error) {
|
|
return p.dir.DataDir().String(), nil
|
|
}
|
|
|
|
// Tmp implements core.Project
|
|
func (p *Project) Tmp() (path string, err error) {
|
|
return p.dir.TempDir().String(), nil
|
|
}
|
|
|
|
// DefaultPrivateKey implements core.Project
|
|
func (p *Project) DefaultPrivateKey() (path string, err error) {
|
|
defaultPrivateKeyPath := p.dir.DataDir().Join("insecure_private_key")
|
|
return defaultPrivateKeyPath.String(), nil
|
|
}
|
|
|
|
// Host implements core.Project
|
|
func (p *Project) Host() (host core.Host, err error) {
|
|
if host, err = p.basis.Host(); err != nil {
|
|
return
|
|
}
|
|
if s, ok := host.(core.Seeder); ok {
|
|
list, err := s.Seeds()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
list = append(list, p)
|
|
if err = s.Seed(list...); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// MachineIndex implements core.Project
|
|
func (p *Project) TargetIndex() (index core.TargetIndex, err error) {
|
|
return p.basis.TargetIndex()
|
|
}
|
|
|
|
// 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,
|
|
},
|
|
),
|
|
)
|
|
}
|
|
|
|
// TargetNames implements core.Project
|
|
func (p *Project) TargetNames() ([]string, error) {
|
|
var names []string
|
|
for _, t := range p.project.Targets {
|
|
names = append(names, t.Name)
|
|
}
|
|
return names, nil
|
|
}
|
|
|
|
// TargetIds implements core.Project
|
|
func (p *Project) TargetIds() ([]string, error) {
|
|
var ids []string
|
|
for _, t := range p.project.Targets {
|
|
ids = append(ids, t.ResourceId)
|
|
}
|
|
return ids, nil
|
|
}
|
|
|
|
// Targets
|
|
func (p *Project) Targets() ([]core.Target, error) {
|
|
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 {
|
|
return p.project.Name
|
|
}
|
|
|
|
// Resource ID for this project
|
|
func (p *Project) ResourceId() string {
|
|
return p.project.ResourceId
|
|
}
|
|
|
|
// 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{
|
|
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
|
|
}
|
|
|
|
if t.dir == nil {
|
|
if t.dir, err = p.dir.Target(t.target.Name); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
// 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{} {
|
|
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)
|
|
|
|
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(task.CliArgs, p.jobInfo, p.dir),
|
|
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 result != nil {
|
|
cmdErr.exitCode = result.(int32)
|
|
}
|
|
|
|
return cmdErr
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// 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.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.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
|
|
}
|
|
// TODO: get provider info here too/generate full machine config?
|
|
// We know that these are machines so, save the Machine record
|
|
machineRecord := &vagrant_server.Target_Machine{}
|
|
// TODO: create a new box for now. This should be querying the box collection
|
|
// for a box that matches the provided provider and name
|
|
doxDir := filepath.Join(p.basis.dir.DataDir().String(), "boxes/hashicorp-VAGRANTSLASH-bionic64/1.0.282/virtualbox/")
|
|
b, erro := NewBox(
|
|
BoxWithBasis(p.basis),
|
|
BoxWithName(t.ConfigVm.Box),
|
|
// TODO: need to get this box info from the vagrantfile/provider
|
|
BoxWithProvider("virtualbox"),
|
|
BoxWithVersion("1.0.282"),
|
|
BoxWithDirectory(doxDir),
|
|
)
|
|
if erro != nil {
|
|
return erro
|
|
}
|
|
machineRecord.Box = b.ToProto()
|
|
machineRecordAny, erro := anypb.New(machineRecord)
|
|
if err != nil {
|
|
p.logger.Debug("Error generating machine record for target",
|
|
"project", p.Name(),
|
|
"target", t.Name,
|
|
)
|
|
return erro
|
|
}
|
|
_, 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,
|
|
Record: machineRecordAny,
|
|
},
|
|
},
|
|
)
|
|
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
|
|
}
|
|
|
|
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()
|
|
|
|
// add project related arguments
|
|
args = append(args,
|
|
argmapper.Typed(p),
|
|
argmapper.Named("project", p),
|
|
argmapper.Named("project_ui", p.UI),
|
|
)
|
|
|
|
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)
|
|
}
|
|
|
|
// options is the configuration to construct a new Project. Some
|
|
// configuration is set directly on the Project. This is only used for
|
|
// intermediate values that need to be processed further before initializing
|
|
// the project.
|
|
type options struct {
|
|
Config *config.Project
|
|
}
|
|
|
|
// 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)
|