Chris Roberts cd222f4622
Clean loading UI usage. Add an extra logging flag.
Use the UI provided by the built client instead of creating a
new one. Add support for an extra `v` flag when enabling logging
via flag. An exclude function is added to the logger to prevent
outputting log lines where the content is extremely long for trace
messages. Adding the extra `v` will prevent the suppression and
all log output will be displayed.
2022-04-25 12:24:24 -05:00

417 lines
9.8 KiB
Go

package cli
//go:generate go-bindata -nomemcopy -nometadata -pkg datagen -o datagen/datagen.go -prefix data/ data/...
import (
"bytes"
"context"
"fmt"
"io"
"io/ioutil"
"os"
"sort"
"text/tabwriter"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-plugin"
"github.com/mitchellh/cli"
"github.com/mitchellh/go-glint"
"github.com/hashicorp/vagrant-plugin-sdk/terminal"
"github.com/hashicorp/vagrant/internal/core"
"github.com/hashicorp/vagrant/internal/pkg/signalcontext"
"github.com/hashicorp/vagrant/internal/version"
)
const (
// EnvLogLevel is the env var to set with the log level.
EnvLogLevel = "VAGRANT_LOG_LEVEL"
// EnvPlain is the env var that can be set to force plain output mode.
EnvPlain = "VAGRANT_PLAIN"
)
var (
// cliName is the name of this CLI.
cliName = "vagrant"
// commonCommands are the commands that are deemed "common" and shown first
// in the CLI help output.
commonCommands = []string{
"up",
"destroy",
"halt",
"status",
"reload",
}
// hiddenCommands are not shown in CLI help output.
hiddenCommands = map[string]struct{}{
"plugin-run": {},
}
ExposeDocs bool
)
// Main runs the CLI with the given arguments and returns the exit code.
// The arguments SHOULD include argv[0] as the program name.
func Main(args []string) int {
// Clean up all our plugins so we don't leave any dangling processes.
// Note that this is a "just in case" catch. We should be properly cleaning
// up plugin processes by calling Close on all the resources we use.
defer plugin.CleanupClients()
// Initialize our logger based on env vars
args, log, logOutput, err := logger(args)
if err != nil {
panic(err)
}
// Log our versions
vsn := version.GetVersion()
log.Info("vagrant version",
"full_string", vsn.FullVersionNumber(true),
"version", vsn.Version,
"prerelease", vsn.VersionPrerelease,
"metadata", vsn.VersionMetadata,
"revision", vsn.Revision,
)
// Build our cancellation context
ctx, closer := signalcontext.WithInterrupt(context.Background(), log)
defer closer()
// Get our base command
base, commands, err := Commands(ctx, args, log, logOutput)
if err != nil {
panic(err)
}
defer base.Close()
// Build the CLI
cli := &cli.CLI{
Name: args[0],
Args: args[1:],
Commands: commands,
Autocomplete: true,
AutocompleteNoDefaultFlags: true,
HelpFunc: GroupedHelpFunc(cli.BasicHelpFunc(cliName)),
}
// Run the CLI
exitCode, err := cli.Run()
if err != nil {
log.Error("cli run failed", "error", err)
panic(err)
}
return exitCode
}
// commands returns the map of commands that can be used to initialize a CLI.
func Commands(
ctx context.Context,
args []string,
log hclog.Logger,
logOutput io.Writer,
opts ...Option,
) (*baseCommand, map[string]cli.CommandFactory, error) {
commands := make(map[string]cli.CommandFactory)
bc := &baseCommand{
Ctx: ctx,
Log: log,
LogOutput: logOutput,
}
// fetch plugin builtin commands
commands["plugin-run"] = func() (cli.Command, error) {
return &PluginCommand{
baseCommand: bc,
}, nil
}
// If running a builtin don't do all the setup
if len(args) > 1 && args[1] == "plugin-run" {
return bc, commands, nil
}
baseCommand, err := BaseCommand(ctx, log, logOutput,
WithArgs(args),
)
if err != nil {
return nil, nil, err
}
s := baseCommand.client.UI().Status()
s.Update("Loading Vagrant...")
result, err := baseCommand.client.Commands(ctx, nil, baseCommand.Modifier())
if err != nil {
s.Step(terminal.StatusError, "Failed to load Vagrant!")
return nil, nil, err
}
s.Step(terminal.StatusOK, "Vagrant loaded!")
s.Close()
// Set plain mode if set
if os.Getenv(EnvPlain) != "" {
baseCommand.globalOptions = append(baseCommand.globalOptions,
WithUI(terminal.NonInteractiveUI(ctx)))
}
// aliases is a list of command aliases we have. The key is the CLI
// command (the alias) and the value is the existing target command.
aliases := map[string]string{}
// fetch remaining builtin commands
commands["version"] = func() (cli.Command, error) {
return &VersionCommand{
baseCommand: baseCommand,
VersionInfo: version.GetVersion(),
}, nil
}
// add dynamic commands
// TODO(spox): reverse the setup here so we load
// dynamic commands first and then define
// any builtin commands on top so the builtin
// commands have proper precedence.
for i := 0; i < len(result.Commands); i++ {
n := result.Commands[i]
flgs, _ := core.FlagOption(n.Flags)
if _, ok := commands[n.Name]; !ok {
commands[n.Name] = func() (cli.Command, error) {
return &DynamicCommand{
baseCommand: baseCommand,
name: n.Name,
synopsis: n.Synopsis,
help: n.Help,
flags: flgs,
flagData: make(map[string]interface{}),
}, nil
}
}
}
// fetch all known plugin commands
commands["plugin"] = func() (cli.Command, error) {
return &PluginCommand{
baseCommand: baseCommand,
}, nil
}
commands["version"] = func() (cli.Command, error) {
return &VersionCommand{
baseCommand: baseCommand,
VersionInfo: version.GetVersion(),
}, nil
}
// register our aliases
for from, to := range aliases {
commands[from] = commands[to]
}
return baseCommand, commands, nil
}
// logger returns the logger to use for the CLI. Output, level, etc. are
// determined based on environment variables if set.
func logger(args []string) ([]string, hclog.Logger, io.Writer, error) {
app := args[0]
verbose := false
// Determine our log level if we have any. First override we check is env var
level := hclog.NoLevel
if v := os.Getenv(EnvLogLevel); v != "" {
level = hclog.LevelFromString(v)
if level == hclog.NoLevel {
return nil, nil, nil, fmt.Errorf("%s value %q is not a valid log level", EnvLogLevel, v)
}
}
// Process arguments looking for `-v` flags to control the log level.
// This overrides whatever the env var set.
var outArgs []string
for _, arg := range args {
if len(arg) != 0 && arg[0] != '-' {
outArgs = append(outArgs, arg)
continue
}
switch arg {
case "-v":
if level == hclog.NoLevel || level > hclog.Info {
level = hclog.Info
}
case "-vv":
if level == hclog.NoLevel || level > hclog.Debug {
level = hclog.Debug
}
case "-vvv":
if level == hclog.NoLevel || level > hclog.Trace {
level = hclog.Trace
}
case "-vvvv":
if level == hclog.NoLevel || level > hclog.Trace {
level = hclog.Trace
}
verbose = true
default:
outArgs = append(outArgs, arg)
}
}
// Default output is nowhere unless we enable logging.
var output io.Writer = ioutil.Discard
color := hclog.ColorOff
if level != hclog.NoLevel {
output = os.Stderr
color = hclog.AutoColor
}
// Since some log line can get extremely verbose depending on what
// fields are included, this will suppress overly long trace lines
// unless we are in verbose mode.
exclude := func(level hclog.Level, msg string, args ...interface{}) bool {
if level != hclog.Trace || verbose {
return false
}
for _, a := range args {
if len(fmt.Sprintf("%v", a)) > 150 {
return true
}
}
return false
}
logger := hclog.New(&hclog.LoggerOptions{
Name: app,
Level: level,
Color: color,
Output: output,
Exclude: exclude,
})
return outArgs, logger, output, nil
}
func GroupedHelpFunc(f cli.HelpFunc) cli.HelpFunc {
return func(commands map[string]cli.CommandFactory) string {
var buf bytes.Buffer
d := glint.New()
d.SetRenderer(&glint.TerminalRenderer{
Output: &buf,
// We set rows/cols here manually. The important bit is the cols
// needs to be wide enough so glint doesn't clamp any text and
// lets the terminal just autowrap it. Rows doesn't make a big
// difference.
Rows: 10,
Cols: 180,
})
// Header
d.Append(glint.Style(
glint.Text("Welcome to Vagrant"),
glint.Bold(),
))
d.Append(glint.Layout(
glint.Style(
glint.Text("Docs:"),
glint.Color("lightBlue"),
),
glint.Text(" "),
glint.Text("https://vagrantup.com"),
).Row())
d.Append(glint.Layout(
glint.Style(
glint.Text("Version:"),
glint.Color("green"),
),
glint.Text(" "),
glint.Text(version.GetVersion().VersionNumber()),
).Row())
d.Append(glint.Text(""))
// Usage
d.Append(glint.Layout(
glint.Style(
glint.Text("Usage:"),
glint.Color("lightMagenta"),
),
glint.Text(" "),
glint.Text(cliName),
glint.Text(" "),
glint.Text("[-version] [-help] [-autocomplete-(un)install] <command> [args]"),
).Row())
d.Append(glint.Text(""))
// Add common commands
helpCommandsSection(d, "Common commands", commonCommands, commands)
// Make our other commands
ignoreMap := map[string]struct{}{}
for k := range hiddenCommands {
ignoreMap[k] = struct{}{}
}
for _, k := range commonCommands {
ignoreMap[k] = struct{}{}
}
var otherCommands []string
for k := range commands {
if _, ok := ignoreMap[k]; ok {
continue
}
otherCommands = append(otherCommands, k)
}
sort.Strings(otherCommands)
// Add other commands
helpCommandsSection(d, "Other commands", otherCommands, commands)
d.RenderFrame()
return buf.String()
}
}
func helpCommandsSection(
d *glint.Document,
header string,
commands []string,
factories map[string]cli.CommandFactory,
) {
// Header
d.Append(glint.Style(
glint.Text(header),
glint.Bold(),
))
// Build our commands
var b bytes.Buffer
tw := tabwriter.NewWriter(&b, 0, 2, 6, ' ', 0)
for _, k := range commands {
fn, ok := factories[k]
if !ok {
continue
}
cmd, err := fn()
if err != nil {
panic(fmt.Sprintf("failed to load %q command: %s", k, err))
}
fmt.Fprintf(tw, "%s\t%s\n", k, cmd.Synopsis())
}
tw.Flush()
d.Append(glint.Layout(
glint.Text(b.String()),
).PaddingLeft(2))
}
var helpText = map[string][2]string{}