package cli import ( "context" "errors" "fmt" "io" "regexp" "strconv" "strings" "github.com/DavidGamba/go-getoptions" "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-multierror" "github.com/hashicorp/vagrant-plugin-sdk/component" "github.com/hashicorp/vagrant-plugin-sdk/helper/paths" "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/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 *clientpkg.Basis // optional project to run operations within project *clientpkg.Project // optional target to run operations against target *clientpkg.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 // flagPlain is whether the output should be in plain mode. flagPlain bool // flagRemote is whether to execute using a remote runner or use // a local runner. flagRemote bool // flagBasis is the basis to work within. flagBasis string // flagProject is the project to work within. flagProject string // flagTarget is the machine to target. flagTarget string // 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 } // 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) { c.Log.Trace("starting command closing") if closer, ok := c.ui.(io.Closer); ok && closer != nil { c.Log.Trace("closing command ui") if e := closer.Close(); e != nil { err = multierror.Append(err, e) } } if c.client != nil { c.Log.Trace("closing command client") if e := c.client.Close(); e != nil { err = multierror.Append(err, e) } } 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, flagData: map[*component.CommandFlag]interface{}{}, } // 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.UI == nil { c.UI = terminal.ConsoleUI(context.Background()) } if c.Args, err = bc.Parse(c.Flags, c.Args, true); err != nil { c.UI.Output(clierrors.Humanize(err), terminal.WithErrorStyle()) return nil, err } // From the command side, the basis is simply where an extra Vagrantfile can // live, as well as our storage context if bc.flagBasis == "" { bc.flagBasis = "default" } 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 } // We always have a basis, so load that if bc.basis, err = bc.client.LoadBasis(bc.flagBasis); err != nil { return nil, err } // A project is optional (though generally needed) and there are // two possibilites for how we load the project. if bc.flagProject != "" { // The first is that we are given a specific project that should be // used within the defined basis. So lets load that. if bc.project, err = bc.basis.LoadProject(bc.flagProject); err != nil { return nil, err } } else { if bc.project, err = bc.basis.DetectProject(); err != nil { return nil, err } } // Load in basis vagrantfile if there is one if err = bc.basis.LoadVagrantfile(); err != nil { return nil, err } // And if we have a project, load that vagrantfile too if bc.project != nil { if err = bc.project.LoadVagrantfile(); 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) } // Init our UI first so we can write output to the user immediately. ui := baseCfg.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 } // Reset the UI to plain if that was set if c.flagPlain { c.ui = terminal.NonInteractiveUI(c.Ctx) } // 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 Task(context.Context, *vagrant_server.Job_RunOp, client.JobModifier) (*vagrant_server.Job_RunResult, error) //CreateTask() *vagrant_server.Task } // 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.basis != nil { j.Basis = c.basis.Ref() } if c.project != nil { j.Project = c.project.Ref() } if c.target != nil { j.Target = c.target.Ref() } } } // 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: "plain", Description: "Plain output: no colors, no animation", DefaultValue: "false", Type: component.FlagBool, }, { LongName: "basis", Description: "Basis to operate within", DefaultValue: "default", Type: component.FlagString, }, { LongName: "target", Description: "Target to apply command", Type: component.FlagString, }, } 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( flags []*component.CommandFlag, args []string, passThrough bool, ) ([]string, error) { opt := c.generateCliFlags(flags) if passThrough { opt.SetUnknownMode(getoptions.Pass) } else { opt.SetUnknownMode(getoptions.Fail) } opt.SetMode(getoptions.Bundling) c.Log.Warn("parsing arguments with flags", "args", args, "flags", flags) remainArgs, err := opt.Parse(args) if err != nil { return nil, err } for _, f := range flags { if called := opt.Called(f.LongName); !called { c.Log.Error("flag was not called", "name", f.LongName) continue } c.Log.Warn("flag was called", "name", f.LongName) if f.Type == component.FlagString { c.flagData[f] = opt.Value(f.LongName) continue } val := true if strings.HasPrefix(opt.CalledAs(f.LongName), "no-") { val = false } c.flagData[f] = val } return remainArgs, nil } func (c *baseCommand) generateCliFlags(set []*component.CommandFlag) *getoptions.GetOpt { opt := getoptions.New() opt.SetUnknownMode(getoptions.Pass) // TODO: make this configurable for _, f := range set { opts := []getoptions.ModifyFn{} if f.Description != "" { opts = append(opts, opt.Description(f.Description)) } // if f.ShortName != "" { // opts = append(opts, opt.Alias(f.ShortName)) // } switch f.Type { case component.FlagBool: opts = append(opts, opt.Alias("no-"+f.LongName)) b, _ := strconv.ParseBool(f.DefaultValue) opt.Bool(f.LongName, b, opts...) case component.FlagString: opt.String(f.LongName, f.DefaultValue, opts...) } } return opt } // 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[-0-9A-Za-z_]+)$`) )