vaguerent/internal/core/project.go
2022-04-25 12:23:57 -05:00

384 lines
9.5 KiB
Go

package core
import (
"context"
"io"
"sync"
"github.com/golang/protobuf/proto"
"github.com/hashicorp/go-argmapper"
"github.com/hashicorp/go-hclog"
"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/terminal"
"github.com/hashicorp/vagrant/internal/config"
"github.com/hashicorp/vagrant/internal/factory"
"github.com/hashicorp/vagrant/internal/plugin"
"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 {
basis *Basis
config *config.Project
logger hclog.Logger
machines map[string]*Machine
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
// jobInfo is the base job info for executed functions.
jobInfo *component.JobInfo
// This lock only needs to be held currently to protect localClosers.
lock sync.Mutex
// The below are resources we need to close when Close is called, if non-nil
localClosers []io.Closer
// 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) JobInfo() *component.JobInfo {
return p.jobInfo
}
func (p *Project) Environment() *Environment {
return p.env
}
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
}
mdir, err := p.dir.Machine(machine.Name)
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 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
}
return p.machines[name], nil
}
// 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_server.Ref_Project{
ResourceId: p.resourceid,
Name: p.name,
Basis: p.basis.Ref().(*vagrant_server.Ref_Basis),
}
}
func (p *Project) Components(ctx context.Context) (results []*Component, err error) {
if results, err = p.basis.Components(ctx); err != nil {
return
}
for _, cc := range componentCreatorMap {
c, err := cc.Create(ctx, p, "")
if status.Code(err) == codes.Unimplemented {
c = nil
err = nil
}
if err != nil {
// Make sure we clean ourselves up in an error case.
for _, r := range results {
r.Close()
}
return nil, err
}
if c != nil {
results = append(results, c)
}
}
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)
cmd, err := p.basis.component(ctx, component.CommandType, task.Component.Name)
if err != nil {
return err
}
defer cmd.Close()
if _, err = p.specializeComponent(cmd); err != nil {
return
}
result, err := p.callDynamicFunc(
ctx,
p.logger,
(interface{})(nil),
cmd,
cmd.Value.(component.Command).ExecuteFunc(),
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)
return err
}
return
}
// 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()
defer p.lock.Unlock()
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)
}
}
// 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
}
}
p.localClosers = nil
return nil
}
func (p *Project) callDynamicFunc(
ctx context.Context,
log hclog.Logger,
result interface{}, // expected result type
c *Component, // component
f interface{}, // function
args ...argmapper.Arg,
) (interface{}, error) {
// Be sure that the status is closed after every operation so we don't leak
// weird output outside the normal execution.
defer p.UI.Status().Close()
args = append(args,
argmapper.ConverterFunc(p.mappers...),
argmapper.Typed(
p.jobInfo,
p.dir,
p.UI,
),
)
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) 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, *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
}
}
func WithBasis(b *Basis) ProjectOption {
return func(p *Project, opts *options) {
p.basis = b
}
}
func WithProjectDataDir(dir *datadir.Project) ProjectOption {
return func(p *Project, opts *options) {
p.dir = dir
}
}
func WithProjectRef(r *vagrant_server.Ref_Project) ProjectOption {
return func(p *Project, opts *options) {
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(),
&vagrant_server.UpsertProjectRequest{
Project: &vagrant_server.Project{
Name: r.Name,
Path: r.Name,
Basis: r.Basis,
},
},
)
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
}
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
}
}
}
}
var _ *Project = (*Project)(nil)