Adds initial basic support for HCP based configuration in vagrant-go. The initalization process has been updated to remove Vagrantfile parsing from the client, moving it to the runner using init jobs for the basis and the project (if there is one). Detection is done on the file based on extension for Ruby based parsing or HCP based parsing. Current HCP parsing is extremely simple and currently just a base to build off. Config components will be able to implement an `Init` function to handle receiving configuration data from a non-native source file. This will be extended to include a default approach for injecting defined data in the future. Some cleanup was done in the state around validations. Some logging adjustments were applied on the Ruby side for better behavior consistency. VirtualBox provider now caches locale detection to prevent multiple checks every time the driver is initialized.
645 lines
18 KiB
Go
645 lines
18 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package cli
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/go-hclog"
|
|
|
|
"github.com/hashicorp/vagrant-plugin-sdk/component"
|
|
"github.com/hashicorp/vagrant-plugin-sdk/helper/paths"
|
|
"github.com/hashicorp/vagrant-plugin-sdk/internal-shared/cleanup"
|
|
"github.com/hashicorp/vagrant-plugin-sdk/proto/vagrant_plugin_sdk"
|
|
"github.com/hashicorp/vagrant-plugin-sdk/terminal"
|
|
"github.com/hashicorp/vagrant/internal/clicontext"
|
|
"github.com/hashicorp/vagrant/internal/client"
|
|
clientpkg "github.com/hashicorp/vagrant/internal/client"
|
|
"github.com/hashicorp/vagrant/internal/clierrors"
|
|
"github.com/hashicorp/vagrant/internal/config"
|
|
"github.com/hashicorp/vagrant/internal/flags"
|
|
"github.com/hashicorp/vagrant/internal/server/proto/vagrant_server"
|
|
"github.com/hashicorp/vagrant/internal/serverclient"
|
|
)
|
|
|
|
// baseCommand is embedded in all commands to provide common logic and data.
|
|
//
|
|
// The unexported values are not available until after Init is called. Some
|
|
// values are only available in certain circumstances, read the documentation
|
|
// for the field to determine if that is the case.
|
|
type baseCommand struct {
|
|
// Ctx is the base context for the command. It is up to commands to
|
|
// utilize this context so that cancellation works in a timely manner.
|
|
Ctx context.Context
|
|
|
|
// Log is the logger to use.
|
|
Log hclog.Logger
|
|
|
|
// LogOutput is the writer that Log points to. You SHOULD NOT use
|
|
// this directly. We have access to this so you can use
|
|
// hclog.OutputResettable if necessary.
|
|
LogOutput io.Writer
|
|
|
|
//---------------------------------------------------------------
|
|
// The fields below are only available after calling Init.
|
|
|
|
// cfg is the parsed configuration
|
|
cfg *config.Config
|
|
|
|
// UI is used to write to the CLI.
|
|
ui terminal.UI
|
|
|
|
// client for performing operations
|
|
client *clientpkg.Client
|
|
// basis to root these operations within
|
|
basis *vagrant_plugin_sdk.Ref_Basis
|
|
// optional project to run operations within
|
|
project *vagrant_plugin_sdk.Ref_Project
|
|
// optional target to run operations against
|
|
target *vagrant_plugin_sdk.Ref_Target
|
|
|
|
// clientContext is set to the context information for the current
|
|
// connection. This might not exist in the contextStorage yet if this
|
|
// is from an env var or flags.
|
|
clientContext *clicontext.Config
|
|
|
|
// contextStorage is for CLI contexts.
|
|
contextStorage *clicontext.Storage
|
|
|
|
//---------------------------------------------------------------
|
|
// Internal fields that should not be accessed directly
|
|
|
|
// flagColor is whether the output should use colors.
|
|
flagColor bool
|
|
|
|
// flagBasis is the basis to work within.
|
|
flagBasis string
|
|
|
|
// flagProject is the project to work within.
|
|
flagProject string
|
|
|
|
// flagRemote is whether to execute using a remote runner or use
|
|
// a local runner.
|
|
flagRemote bool
|
|
|
|
// flagTarget is the machine to target.
|
|
flagTarget string
|
|
|
|
// flagInteractive is whether the output is interactive
|
|
flagInteractive bool
|
|
|
|
// flagMachineReadable is whether the terminal ui should only output machine readable data
|
|
flagMachineReadable bool
|
|
|
|
// flagConnection contains manual flag-based connection info.
|
|
flagConnection clicontext.Config
|
|
|
|
// flagData contains flag info for command
|
|
flagData map[*component.CommandFlag]interface{}
|
|
|
|
// args that were present after parsing flags
|
|
args []string
|
|
|
|
// options passed in at the global level
|
|
globalOptions []Option
|
|
|
|
// used to store cleanup tasks at close
|
|
cleanup cleanup.Cleanup
|
|
}
|
|
|
|
// Close cleans up any resources that the command created. This should be
|
|
// defered by any CLI command that embeds baseCommand in the Run command.
|
|
func (c *baseCommand) Close() (err error) {
|
|
if c.cleanup == nil {
|
|
return nil
|
|
}
|
|
|
|
err = c.cleanup.Close()
|
|
if err != nil {
|
|
c.Log.Error("failure encountered while closing command",
|
|
"error", err,
|
|
)
|
|
}
|
|
return
|
|
}
|
|
|
|
func BaseCommand(ctx context.Context, log hclog.Logger, logOutput io.Writer, opts ...Option) (bc *baseCommand, err error) {
|
|
bc = &baseCommand{
|
|
Ctx: ctx,
|
|
Log: log,
|
|
LogOutput: logOutput,
|
|
cleanup: cleanup.New(),
|
|
flagData: map[*component.CommandFlag]interface{}{},
|
|
}
|
|
|
|
// Define a cleanup to close the UI if it's set
|
|
bc.cleanup.Do(func() error {
|
|
if bc.ui != nil {
|
|
if closer, ok := bc.ui.(io.Closer); ok && closer != nil {
|
|
return closer.Close()
|
|
}
|
|
}
|
|
return err
|
|
})
|
|
|
|
// Define a cleanup task to close the client if it's set
|
|
bc.cleanup.Do(func() error {
|
|
if bc.client != nil {
|
|
return bc.client.Close()
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
// Get just enough base configuration to
|
|
// allow setting up our client connection
|
|
c := &baseConfig{
|
|
Client: true,
|
|
Flags: bc.flagSet(flagSetConnection, nil),
|
|
}
|
|
|
|
// Apply any options that were passed. These
|
|
// should at least include the arguments so
|
|
// we can extract the flags properly
|
|
for _, opt := range opts {
|
|
opt(c)
|
|
}
|
|
|
|
if c.Args, err = bc.Parse(c.Flags, c.Args, true); err != nil {
|
|
log.Error(clierrors.Humanize(err))
|
|
return nil, err
|
|
}
|
|
|
|
// Set UI
|
|
var ui terminal.UI
|
|
if bc.flagMachineReadable {
|
|
// Set machine readable ui if the --machine-readable flag is provided
|
|
ui = terminal.MachineReadableUI(ctx, terminal.TableFormat)
|
|
} else if !bc.flagInteractive {
|
|
// Set non interactive if the --no-interactive flag is provided
|
|
ui = terminal.NonInteractiveUI(ctx)
|
|
} else if !bc.flagColor {
|
|
// Set basic ui (with no color) if the --no-color flag is provided
|
|
ui = terminal.BasicUI(ctx)
|
|
} else {
|
|
// If no ui related flags are set, create a new one
|
|
ui = terminal.ConsoleUI(ctx)
|
|
}
|
|
bc.ui = ui
|
|
|
|
outputExperimentalWarning := true
|
|
if val, ok := os.LookupEnv("VAGRANT_SUPPRESS_GO_EXPERIMENTAL_WARNING"); ok {
|
|
if v, err := strconv.ParseBool(val); err == nil && v {
|
|
outputExperimentalWarning = false
|
|
}
|
|
}
|
|
if outputExperimentalWarning {
|
|
ui.Output(`This is an experimental version of Vagrant. Please note that some things may
|
|
not work as you expect and this version of Vagrant is not compatible with the
|
|
stable version of Vagrant. For more information about vagrant-go read the docs
|
|
at https://www.vagrantup.com/docs/experimental/vagrant_go. To disable this
|
|
warning set the environment variable 'VAGRANT_SUPPRESS_GO_EXPERIMENTAL_WARNING'.
|
|
`, terminal.WithWarningStyle())
|
|
}
|
|
|
|
homeConfigPath, err := paths.NamedVagrantConfig(bc.flagBasis)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bc.Log.Info("vagrant home directory defined",
|
|
"path", homeConfigPath)
|
|
|
|
// Setup our directory for context storage
|
|
contextStorage, err := clicontext.NewStorage(
|
|
clicontext.WithDir(homeConfigPath.Join("context")))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bc.contextStorage = contextStorage
|
|
|
|
// We use our flag-based connection info if the user set an addr.
|
|
var flagConnection *clicontext.Config
|
|
if v := bc.flagConnection; v.Server.Address != "" {
|
|
flagConnection = &v
|
|
}
|
|
|
|
// Get the context we'll use. The ordering here is purposeful and creates
|
|
// the following precedence: (1) context (2) env (3) flags where the
|
|
// later values override the former.
|
|
|
|
connectOpts := []serverclient.ConnectOption{
|
|
serverclient.FromContext(bc.contextStorage, ""),
|
|
serverclient.FromEnv(),
|
|
serverclient.FromContextConfig(flagConnection),
|
|
}
|
|
bc.clientContext, err = serverclient.ContextConfig(connectOpts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Start building our client options
|
|
clientOpts := []clientpkg.Option{
|
|
clientpkg.WithLogger(bc.Log.ResetNamed("vagrant.client")),
|
|
clientpkg.WithClientConnect(connectOpts...),
|
|
}
|
|
if !bc.flagRemote {
|
|
clientOpts = append(clientOpts, clientpkg.WithLocal())
|
|
}
|
|
|
|
if bc.ui != nil {
|
|
clientOpts = append(clientOpts, clientpkg.WithUI(bc.ui))
|
|
}
|
|
|
|
// And build our client
|
|
bc.client, err = clientpkg.New(ctx, clientOpts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
bc.Log.Info("basis seeding", "name", bc.flagBasis, "path", bc.flagBasis)
|
|
|
|
b, err := bc.client.LoadBasis(bc.flagBasis)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// We always have a basis, so seed the basis
|
|
// ref and initialize it
|
|
bc.basis = &vagrant_plugin_sdk.Ref_Basis{
|
|
Name: b.Name,
|
|
Path: b.Path,
|
|
ResourceId: b.ResourceId,
|
|
}
|
|
if bc.basis, err = bc.client.BasisInit(ctx, bc.Modifier()); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
log.Info("vagrant basis has been initialized",
|
|
"basis", bc.basis,
|
|
)
|
|
|
|
project, err := bc.client.LoadProject(bc.flagProject, bc.basis)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if project != nil {
|
|
bc.project = &vagrant_plugin_sdk.Ref_Project{
|
|
Name: project.Name,
|
|
Path: project.Path,
|
|
ResourceId: project.ResourceId,
|
|
Basis: project.Basis,
|
|
}
|
|
if bc.project, err = bc.client.ProjectInit(ctx, bc.Modifier()); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// There's also a chance we are supposed to be focused on
|
|
// a specific target, so load that if so
|
|
if bc.flagTarget != "" {
|
|
if bc.project == nil {
|
|
return nil, fmt.Errorf("cannot load target without valid project")
|
|
}
|
|
|
|
// if bc.target, err = bc.project.LoadTarget(bc.flagTarget); err != nil {
|
|
// return nil, err
|
|
// }
|
|
}
|
|
|
|
return bc, err
|
|
}
|
|
|
|
// Init initializes the command by parsing flags, parsing the configuration,
|
|
// setting up the project, etc. You can control what is done by using the
|
|
// options.
|
|
//
|
|
// Init should be called FIRST within the Run function implementation. Many
|
|
// options will affect behavior of other functions that can be called later.
|
|
func (c *baseCommand) Init(opts ...Option) (err error) {
|
|
baseCfg := baseConfig{
|
|
Config: true,
|
|
Client: true,
|
|
}
|
|
|
|
for _, opt := range c.globalOptions {
|
|
opt(&baseCfg)
|
|
}
|
|
|
|
for _, opt := range opts {
|
|
opt(&baseCfg)
|
|
}
|
|
|
|
// Set UI
|
|
var ui terminal.UI
|
|
if c.flagMachineReadable {
|
|
// Set machine readable ui if the --machine-readable flag is provided
|
|
ui = terminal.MachineReadableUI(c.Ctx, terminal.TableFormat)
|
|
} else if !c.flagInteractive {
|
|
// Set non interactive if the --no-interactive flag is provided
|
|
ui = terminal.NonInteractiveUI(c.Ctx)
|
|
} else if !c.flagColor {
|
|
// Set basic ui (with no color) if the --no-color flag is provided
|
|
ui = terminal.BasicUI(c.Ctx)
|
|
} else {
|
|
// If no ui related flags are set, use the base config ui
|
|
ui = baseCfg.UI
|
|
}
|
|
// If no ui is provided, then build a new UI
|
|
if ui == nil {
|
|
ui = terminal.ConsoleUI(c.Ctx)
|
|
}
|
|
c.ui = ui
|
|
|
|
// Parse flags
|
|
c.Log.Warn("generating flags", "flags", baseCfg.Flags)
|
|
if c.args, err = c.Parse(baseCfg.Flags, baseCfg.Args, false); err != nil {
|
|
c.ui.Output(clierrors.Humanize(err), terminal.WithErrorStyle())
|
|
return err
|
|
}
|
|
|
|
// Parse the configuration (config does not need to exist)
|
|
// TODO: This should be `c.initConfig(true)`,
|
|
// need to set the basis path first
|
|
c.cfg = &config.Config{}
|
|
|
|
// Validate remote vs. local operations.
|
|
if c.flagRemote && c.target == nil {
|
|
if c.cfg == nil || c.cfg.Runner == nil || !c.cfg.Runner.Enabled {
|
|
err := errors.New(
|
|
"The `-remote` flag was specified but remote operations are not supported\n" +
|
|
"for this project.\n\n" +
|
|
"Remote operations must be manually enabled by using setting the 'runner.enabled'\n" +
|
|
"setting in your Vagrant configuration file. Please see the documentation\n" +
|
|
"on this setting for more information.")
|
|
c.logError(c.Log, "", err)
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type Tasker interface {
|
|
UI() terminal.UI
|
|
Command(context.Context, *vagrant_server.Job_CommandOp, client.JobModifier) (*vagrant_server.Job_CommandResult, error)
|
|
}
|
|
|
|
// Do calls the callback based on the loaded scope. This automatically handles any
|
|
// parallelization, waiting, and error handling. Your code should be
|
|
// thread-safe.
|
|
//
|
|
// Based on the scope the callback may be executed multiple times. When scoped by
|
|
// machine, it will be run against each requested machine. When the scope is basis
|
|
// or project, it will only be run once.
|
|
//
|
|
// If any error is returned, the caller should just exit. The error handling
|
|
// including messaging to the user is handling by this function call.
|
|
//
|
|
// If you want to early exit all the running functions, you should use
|
|
// the callback closure properties to cancel the passed in context. This
|
|
// will stop any remaining callbacks and exit early.
|
|
func (c *baseCommand) Do(ctx context.Context, f func(context.Context, *client.Client, client.JobModifier) error) (finalErr error) {
|
|
return f(ctx, c.client, c.Modifier())
|
|
}
|
|
|
|
func (c *baseCommand) Modifier() client.JobModifier {
|
|
return func(j *vagrant_server.Job) {
|
|
if c.project != nil {
|
|
j.Scope = &vagrant_server.Job_Project{
|
|
Project: c.project,
|
|
}
|
|
return
|
|
}
|
|
if c.basis != nil {
|
|
j.Scope = &vagrant_server.Job_Basis{
|
|
Basis: c.basis,
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// logError logs an error and outputs it to the UI.
|
|
func (c *baseCommand) logError(log hclog.Logger, prefix string, err error) {
|
|
if err == ErrSentinel {
|
|
return
|
|
}
|
|
|
|
log.Error(prefix, "error", err)
|
|
|
|
if prefix != "" {
|
|
prefix += ": "
|
|
}
|
|
c.ui.Output("%s%s", prefix, err, terminal.WithErrorStyle())
|
|
}
|
|
|
|
// flagSet creates the flags for this command. The callback should be used
|
|
// to configure the set with your own custom options.
|
|
func (c *baseCommand) flagSet(bit flagSetBit, f func([]*component.CommandFlag) []*component.CommandFlag) component.CommandFlags {
|
|
set := []*component.CommandFlag{
|
|
{
|
|
LongName: "color",
|
|
Description: "Can be used to disable colored output",
|
|
DefaultValue: "true",
|
|
Type: component.FlagBool,
|
|
},
|
|
{
|
|
LongName: "basis",
|
|
Description: "Basis to operate within",
|
|
DefaultValue: "default",
|
|
Type: component.FlagString,
|
|
},
|
|
{
|
|
LongName: "target",
|
|
Description: "Target to apply command",
|
|
DefaultValue: "",
|
|
Type: component.FlagString,
|
|
},
|
|
{
|
|
LongName: "interactive",
|
|
Description: "Enable non-interactive output",
|
|
DefaultValue: "true",
|
|
Type: component.FlagBool,
|
|
},
|
|
{
|
|
LongName: "machine-readable",
|
|
Description: "Target to apply command",
|
|
DefaultValue: "false",
|
|
Type: component.FlagBool,
|
|
},
|
|
}
|
|
|
|
if bit&flagSetOperation != 0 {
|
|
set = append(set,
|
|
&component.CommandFlag{
|
|
LongName: "remote",
|
|
Description: "Use remote runner to execute",
|
|
DefaultValue: "false",
|
|
Type: component.FlagBool,
|
|
},
|
|
&component.CommandFlag{
|
|
LongName: "remote-source",
|
|
Description: "Override how remote runners source data",
|
|
Type: component.FlagString,
|
|
},
|
|
)
|
|
}
|
|
|
|
if bit&flagSetConnection != 0 {
|
|
set = append(set,
|
|
&component.CommandFlag{
|
|
LongName: "server-addr",
|
|
ShortName: "",
|
|
Description: "Address for the server",
|
|
Type: component.FlagString,
|
|
},
|
|
&component.CommandFlag{
|
|
LongName: "server-tls",
|
|
ShortName: "",
|
|
Description: "Connect to server via TLS",
|
|
DefaultValue: "true",
|
|
Type: component.FlagBool,
|
|
},
|
|
&component.CommandFlag{
|
|
LongName: "server-tls-skip-verify",
|
|
ShortName: "",
|
|
Description: "Skip verification of the TLS certificate advertised by the server",
|
|
DefaultValue: "false",
|
|
Type: component.FlagBool,
|
|
},
|
|
)
|
|
}
|
|
|
|
if f != nil {
|
|
// Configure our values
|
|
set = f(set)
|
|
}
|
|
|
|
return set
|
|
}
|
|
|
|
func (c *baseCommand) Parse(
|
|
set []*component.CommandFlag,
|
|
args []string,
|
|
passThrough bool,
|
|
) ([]string, error) {
|
|
fset := c.generateCliFlags(set)
|
|
if passThrough {
|
|
flags.SetUnknownMode(flags.PassOnUnknown)(fset)
|
|
}
|
|
|
|
c.Log.Warn("parsing arguments with flags", "args", args, "flags", set)
|
|
remainArgs, err := fset.Parse(args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, f := range set {
|
|
pf, err := fset.Flag(f.LongName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Set the default flag values
|
|
switch f.LongName {
|
|
case "basis":
|
|
c.flagBasis = pf.DefaultValue().(string)
|
|
case "color":
|
|
c.flagColor = pf.DefaultValue().(bool)
|
|
case "target":
|
|
if v := pf.DefaultValue(); v != nil {
|
|
c.flagTarget = pf.DefaultValue().(string)
|
|
}
|
|
case "remote":
|
|
c.flagRemote = pf.DefaultValue().(bool)
|
|
case "interactive":
|
|
c.flagInteractive = pf.DefaultValue().(bool)
|
|
case "machine-readable":
|
|
c.flagMachineReadable = pf.DefaultValue().(bool)
|
|
}
|
|
if !pf.Updated() {
|
|
continue
|
|
}
|
|
// Set the given flag values
|
|
switch f.LongName {
|
|
case "basis":
|
|
c.flagBasis = pf.Value().(string)
|
|
case "color":
|
|
c.flagColor = pf.Value().(bool)
|
|
case "target":
|
|
c.flagTarget = pf.Value().(string)
|
|
case "remote":
|
|
c.flagRemote = pf.Value().(bool)
|
|
case "interactive":
|
|
c.flagInteractive = pf.Value().(bool)
|
|
case "machine-readable":
|
|
c.flagMachineReadable = pf.Value().(bool)
|
|
}
|
|
c.flagData[f] = pf.Value()
|
|
}
|
|
return remainArgs, nil
|
|
}
|
|
|
|
func (c *baseCommand) generateCliFlags(set []*component.CommandFlag) *flags.Set {
|
|
fs := flags.NewSet("flags",
|
|
flags.SetErrorMode(flags.ReturnOnError),
|
|
flags.SetUnknownMode(flags.ErrorOnUnknown),
|
|
)
|
|
|
|
for _, f := range set {
|
|
opts := []flags.FlagModifier{}
|
|
if f.Description != "" {
|
|
opts = append(opts, flags.Description(f.Description))
|
|
}
|
|
if f.ShortName != "" {
|
|
opts = append(opts, flags.ShortName(rune(f.ShortName[0])))
|
|
}
|
|
if len(f.Aliases) > 0 {
|
|
opts = append(opts, flags.Alias(f.Aliases...))
|
|
}
|
|
switch f.Type {
|
|
case component.FlagBool:
|
|
b, _ := strconv.ParseBool(f.DefaultValue)
|
|
opts = append(opts, flags.DefaultValue(b))
|
|
fs.DefaultGroup().Bool(f.LongName, opts...)
|
|
case component.FlagString:
|
|
if f.DefaultValue != "" {
|
|
opts = append(opts, flags.DefaultValue(f.DefaultValue))
|
|
}
|
|
fs.DefaultGroup().String(f.LongName, opts...)
|
|
}
|
|
|
|
}
|
|
return fs
|
|
}
|
|
|
|
// flagSetBit is used with baseCommand.flagSet
|
|
type flagSetBit uint
|
|
|
|
const (
|
|
flagSetNone flagSetBit = 1 << iota
|
|
flagSetOperation // shared flags for operations (build, deploy, etc)
|
|
flagSetConnection // shared flags for server connections
|
|
)
|
|
|
|
const MaxStringMapArgs int = 50
|
|
|
|
var (
|
|
// ErrSentinel is a sentinel value that we can return from Init to force an exit.
|
|
ErrSentinel = errors.New("error sentinel")
|
|
|
|
errTargetModeSingle = strings.TrimSpace(`
|
|
This command requires a single targeted machine. You have multiple machines defined
|
|
so you can specify the machine to target using the "-machine" flag.
|
|
`)
|
|
|
|
reTarget = regexp.MustCompile(`^(?P<machine>[-0-9A-Za-z_]+)$`)
|
|
)
|