611 lines
15 KiB
Go

package core
import (
"context"
"fmt"
"reflect"
"strings"
"sync"
"github.com/DavidGamba/go-getoptions/option"
"github.com/golang/protobuf/proto"
"github.com/hashicorp/go-argmapper"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-multierror"
"github.com/mitchellh/mapstructure"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/hashicorp/vagrant-plugin-sdk/component"
sdkcore "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/internal-shared/protomappers"
"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/plugin"
"github.com/hashicorp/vagrant/internal/server/proto/vagrant_server"
"github.com/hashicorp/vagrant/internal/serverclient"
)
// Basis represents the core basis which may
// include one or more projects.
//
// The Close function should be called when
// 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
client *serverclient.VagrantClient
jobInfo *component.JobInfo
lock sync.Mutex
closers []func() error
UI terminal.UI
}
func (b *Basis) Ui() terminal.UI {
return b.UI
}
func (b *Basis) Ref() interface{} {
return &vagrant_server.Ref_Basis{
ResourceId: b.resourceid,
Name: b.name,
}
}
func (b *Basis) JobInfo() *component.JobInfo {
return b.jobInfo
}
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.Trace("running init for basis")
f := b.factories[component.CommandType]
result = &vagrant_server.Job_InitResult{}
for _, name := range f.Registered() {
c, err := componentCreatorMap[component.CommandType].Create(context.Background(), b, name)
if err != nil {
b.logger.Error("failed to start plugin", "name", name, "error", err)
continue
}
cmd, ok := c.Value.(sdkcore.Command)
if !ok {
b.logger.Error("failed to convert to command component")
continue
}
b.logger.Trace("started a new plugin for init", "name", name)
syn, err := cmd.Synopsis()
if err != nil {
b.logger.Error("failed to get synopsis for command "+name, "error", err)
}
hlp, err := cmd.Help()
if err != nil {
b.logger.Error("failed to get help for command "+name, "error", err)
}
flgs, err := cmd.Flags()
if err != nil {
b.logger.Error("failed to get flags for command "+name, "error", err)
}
result.Commands = append(
result.Commands,
&vagrant_server.Job_Command{
Name: name,
Synopsis: syn,
Help: hlp,
Flags: FlagsToProtoMapper(flgs),
},
)
}
return
}
func FlagsToProtoMapper(input []*option.Option) []*vagrant_server.Job_Flag {
output := []*vagrant_server.Job_Flag{}
for _, f := range input {
var flagType vagrant_server.Job_Flag_Type
switch f.OptType {
case option.StringType:
flagType = vagrant_server.Job_Flag_STRING
case option.BoolType:
flagType = vagrant_server.Job_Flag_BOOL
}
// TODO: get aliases
j := &vagrant_server.Job_Flag{
LongName: f.Name,
ShortName: f.Name,
Description: f.Description,
DefaultValue: f.DefaultStr,
Type: flagType,
}
output = append(output, j)
}
return output
}
func ProtoToFlagsMapper(input []*vagrant_server.Job_Flag) (opt []*option.Option, err error) {
opt = []*option.Option{}
for _, f := range input {
var newOpt *option.Option
switch f.Type {
case vagrant_server.Job_Flag_STRING:
newOpt = option.New(f.LongName, option.StringType)
case vagrant_server.Job_Flag_BOOL:
newOpt = option.New(f.LongName, option.BoolType)
}
newOpt.Description = f.Description
newOpt.DefaultStr = f.DefaultValue
opt = append(opt, newOpt)
}
return opt, err
}
// NewBasis creates a new Basis with the given options.
func NewBasis(ctx context.Context, opts ...BasisOption) (b *Basis, err error) {
b = &Basis{
logger: hclog.L(),
jobInfo: &component.JobInfo{},
factories: map[component.Type]*factory.Factory{
component.CommandType: plugin.BaseFactories[component.CommandType],
component.CommunicatorType: plugin.BaseFactories[component.CommunicatorType],
component.ConfigType: plugin.BaseFactories[component.ConfigType],
component.GuestType: plugin.BaseFactories[component.GuestType],
component.HostType: plugin.BaseFactories[component.HostType],
component.ProviderType: plugin.BaseFactories[component.ProviderType],
component.ProvisionerType: plugin.BaseFactories[component.ProvisionerType],
component.SyncedFolderType: plugin.BaseFactories[component.SyncedFolderType],
},
}
for _, opt := range opts {
opt(b)
}
// If we don't have a data directory set, lets do that now
if b.dir == nil {
return nil, fmt.Errorf("WithDataDir must be specified")
}
if b.UI == nil {
b.UI = terminal.ConsoleUI(ctx)
}
if len(b.mappers) == 0 {
b.mappers, err = argmapper.NewFuncList(protomappers.All,
argmapper.Logger(b.logger),
)
if err != nil {
return
}
}
envMapper, _ := argmapper.NewFunc(EnvironmentProto)
b.mappers = append(b.mappers, envMapper)
if b.client == nil {
panic("b.client should never be nil")
}
if b.config == nil {
b.config, err = config.Load("", "")
}
b.env, err = NewEnvironment(ctx,
WithHomePath(b.dir.Dir.RootDir()),
)
if err != nil {
return nil, err
}
b.logger.Info("basis initialized")
return
}
// TODO: put this in a better place
func EnvironmentProto(input *Environment) (*vagrant_plugin_sdk.Args_Project, error) {
var result vagrant_plugin_sdk.Args_Project
pathToStringHook := func(f, t reflect.Type, data interface{}) (interface{}, error) {
if f != reflect.TypeOf(path.NewPath(".")) {
return data, nil
}
if t.Kind() != reflect.String {
return data, nil
}
// Convert it
path := data.(path.Path)
return path.String(), nil
}
decoder, err := mapstructure.NewDecoder(
&mapstructure.DecoderConfig{
DecodeHook: pathToStringHook,
Metadata: nil,
Result: &result,
},
)
if err != nil {
return nil, err
}
return &result, decoder.Decode(input)
}
func (b *Basis) LoadProject(ctx context.Context, popts ...ProjectOption) (p *Project, err error) {
// Create our project
p = &Project{
basis: b,
logger: b.logger.Named("project"),
mappers: b.mappers,
factories: b.factories,
machines: map[string]*Machine{},
UI: b.UI,
env: b.env,
}
var opts options
// Apply any options provided
for _, opt := range popts {
opt(p, &opts)
}
// 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 {
return
}
m := &Machine{
name: mCfg.Name,
config: mCfg,
logger: p.logger.Named(mCfg.Name),
dir: d,
UI: terminal.ConsoleUI(ctx),
}
p.machines[m.name] = m
}
return
}
func (b *Basis) Close() error {
for _, c := range b.closers {
c()
}
return nil
}
func (b *Basis) Components(ctx context.Context) ([]*Component, error) {
var results []*Component
for _, cc := range componentCreatorMap {
c, err := cc.Create(ctx, b, "")
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 (b *Basis) component(ctx context.Context, typ component.Type, name string) (*Component, error) {
return componentCreatorMap[typ].Create(ctx, b, name)
}
func (b *Basis) specializeComponent(c *Component) (cmp plugin.PluginMetadata, err error) {
var ok bool
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("vagrant_service_endpoint", b.client.ServerTarget())
return
}
func (b *Basis) Run(ctx context.Context, task *vagrant_server.Task) (err error) {
b.logger.Debug("running new task", "basis", b, "task", task)
cmd, err := b.component(ctx, component.CommandType, task.Component.Name)
if err != nil {
return err
}
if _, err = b.specializeComponent(cmd); err != nil {
return
}
result, err := b.callDynamicFunc(
ctx,
b.logger,
(interface{})(nil),
cmd,
cmd.Value.(component.Command).ExecuteFunc(),
argmapper.Typed(task.CliArgs),
)
if err != nil || result == nil || result.(int64) != 0 {
b.logger.Error("failed to execute command", "type", component.CommandType, "name", task.Component.Name, "error", err)
return err
}
return
}
// startPlugin starts a plugin with the given type and name. The returned
// value must be closed to clean up the plugin properly.
func (b *Basis) startPlugin(
ctx context.Context,
typ component.Type,
n string,
) (*plugin.Instance, error) {
log := b.logger.Named(strings.ToLower(typ.String()))
f, ok := b.factories[typ]
if !ok {
return nil, fmt.Errorf("unknown factory: %T", typ)
}
// Get the factory function for this type
fn := f.Func(n)
if fn == nil {
return nil, fmt.Errorf("unknown type: %q", n)
}
// Call the factory to get our raw value (interface{} type)
fnResult := fn.Call(argmapper.Typed(ctx, log))
if err := fnResult.Err(); err != nil {
return nil, err
}
log.Info("initialized component", "type", typ.String())
raw := fnResult.Out(0)
// If we have a plugin.Instance then we can extract other information
// from this plugin. We accept pure factories too that don't return
// this so we type-check here.
pinst, ok := raw.(*plugin.Instance)
if !ok {
pinst = &plugin.Instance{
Component: raw,
Close: func() {},
}
}
return pinst, nil
}
func (b *Basis) callDynamicFunc(
ctx context.Context,
log hclog.Logger,
result interface{}, // expected result type
c *Component, // component
f interface{}, // function
args ...argmapper.Arg,
) (interface{}, error) {
// We allow f to be a *mapper.Func because our plugin system creates
// a func directly due to special argument types.
// TODO: test
rawFunc, ok := f.(*argmapper.Func)
if !ok {
var err error
rawFunc, err = argmapper.NewFunc(f, argmapper.Logger(log))
if err != nil {
return nil, err
}
}
// Be sure that the status is closed after every operation so we don't leak
// weird output outside the normal execution.
defer b.UI.Status().Close()
args = append(args,
argmapper.ConverterFunc(b.mappers...),
argmapper.Typed(
b.jobInfo,
b.dir,
b.UI,
),
)
// 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
callResult := rawFunc.Call(args...)
if err := callResult.Err(); err != nil {
return nil, err
}
raw := callResult.Out(0)
// If we don't have an expected result type, then just return as-is.
// Otherwise, we need to verify the result type matches properly.
if result == nil {
return raw, nil
}
// Verify
interfaceType := reflect.TypeOf(result).Elem()
if rawType := reflect.TypeOf(raw); !rawType.Implements(interfaceType) {
return nil, status.Errorf(codes.FailedPrecondition,
"operation expected result type %s, got %s",
interfaceType.String(),
rawType.String())
}
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)
}
func (b *Basis) doOperation(ctx context.Context, log hclog.Logger, op operation) (interface{}, proto.Message, error) {
return doOperation(ctx, log, b, op)
}
// BasisOption is used to set options for NewBasis.
type BasisOption func(*Basis)
// WithClient sets the API client to use.
func WithClient(client *serverclient.VagrantClient) BasisOption {
return func(b *Basis) {
b.client = client
}
}
// 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 }
}
// 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 }
}
func WithBasisConfig(c *config.Config) BasisOption {
return func(b *Basis) { b.config = c }
}
// WithComponents sets the factories for components.
func WithComponents(fs map[component.Type]*factory.Factory) BasisOption {
return func(b *Basis) { b.factories = fs }
}
// 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...) }
}
// 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 }
}
// WithJobInfo sets the base job info used for any executed operations.
func WithJobInfo(info *component.JobInfo) BasisOption {
return func(b *Basis) { b.jobInfo = info }
}
func WithBasisDataDir(dir *datadir.Basis) BasisOption {
return func(b *Basis) { b.dir = dir }
}
func WithBasisRef(r *vagrant_server.Ref_Basis) BasisOption {
return func(b *Basis) {
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(
context.Background(),
&vagrant_server.UpsertBasisRequest{
Basis: &vagrant_server.Basis{
Name: r.Name,
Path: r.Name,
},
},
)
if err != nil {
panic("failed to upsert basis") // TODO(spox): don't panic
}
basis = result.Basis
} else {
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
}
basis = result.Basis
}
b.name = basis.Name
b.resourceid = basis.ResourceId
// 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
}
}
}
}
var _ *Basis = (*Basis)(nil)