mirror of https://github.com/coder/coder.git
347 lines
9.9 KiB
Go
347 lines
9.9 KiB
Go
package clibase
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/go-multierror"
|
|
"github.com/spf13/pflag"
|
|
"golang.org/x/xerrors"
|
|
)
|
|
|
|
type ValueSource string
|
|
|
|
const (
|
|
ValueSourceNone ValueSource = ""
|
|
ValueSourceFlag ValueSource = "flag"
|
|
ValueSourceEnv ValueSource = "env"
|
|
ValueSourceYAML ValueSource = "yaml"
|
|
ValueSourceDefault ValueSource = "default"
|
|
)
|
|
|
|
// Option is a configuration option for a CLI application.
|
|
type Option struct {
|
|
Name string `json:"name,omitempty"`
|
|
Description string `json:"description,omitempty"`
|
|
// Required means this value must be set by some means. It requires
|
|
// `ValueSource != ValueSourceNone`
|
|
// If `Default` is set, then `Required` is ignored.
|
|
Required bool `json:"required,omitempty"`
|
|
|
|
// Flag is the long name of the flag used to configure this option. If unset,
|
|
// flag configuring is disabled.
|
|
Flag string `json:"flag,omitempty"`
|
|
// FlagShorthand is the one-character shorthand for the flag. If unset, no
|
|
// shorthand is used.
|
|
FlagShorthand string `json:"flag_shorthand,omitempty"`
|
|
|
|
// Env is the environment variable used to configure this option. If unset,
|
|
// environment configuring is disabled.
|
|
Env string `json:"env,omitempty"`
|
|
|
|
// YAML is the YAML key used to configure this option. If unset, YAML
|
|
// configuring is disabled.
|
|
YAML string `json:"yaml,omitempty"`
|
|
|
|
// Default is parsed into Value if set.
|
|
Default string `json:"default,omitempty"`
|
|
// Value includes the types listed in values.go.
|
|
Value pflag.Value `json:"value,omitempty"`
|
|
|
|
// Annotations enable extensions to clibase higher up in the stack. It's useful for
|
|
// help formatting and documentation generation.
|
|
Annotations Annotations `json:"annotations,omitempty"`
|
|
|
|
// Group is a group hierarchy that helps organize this option in help, configs
|
|
// and other documentation.
|
|
Group *Group `json:"group,omitempty"`
|
|
|
|
// UseInstead is a list of options that should be used instead of this one.
|
|
// The field is used to generate a deprecation warning.
|
|
UseInstead []Option `json:"use_instead,omitempty"`
|
|
|
|
Hidden bool `json:"hidden,omitempty"`
|
|
|
|
ValueSource ValueSource `json:"value_source,omitempty"`
|
|
}
|
|
|
|
// optionNoMethods is just a wrapper around Option so we can defer to the
|
|
// default json.Unmarshaler behavior.
|
|
type optionNoMethods Option
|
|
|
|
func (o *Option) UnmarshalJSON(data []byte) error {
|
|
// If an option has no values, we have no idea how to unmarshal it.
|
|
// So just discard the json data.
|
|
if o.Value == nil {
|
|
o.Value = &DiscardValue
|
|
}
|
|
|
|
return json.Unmarshal(data, (*optionNoMethods)(o))
|
|
}
|
|
|
|
func (o Option) YAMLPath() string {
|
|
if o.YAML == "" {
|
|
return ""
|
|
}
|
|
var gs []string
|
|
for _, g := range o.Group.Ancestry() {
|
|
gs = append(gs, g.YAML)
|
|
}
|
|
return strings.Join(append(gs, o.YAML), ".")
|
|
}
|
|
|
|
// OptionSet is a group of options that can be applied to a command.
|
|
type OptionSet []Option
|
|
|
|
// UnmarshalJSON implements json.Unmarshaler for OptionSets. Options have an
|
|
// interface Value type that cannot handle unmarshalling because the types cannot
|
|
// be inferred. Since it is a slice, instantiating the Options first does not
|
|
// help.
|
|
//
|
|
// However, we typically do instantiate the slice to have the correct types.
|
|
// So this unmarshaller will attempt to find the named option in the existing
|
|
// set, if it cannot, the value is discarded. If the option exists, the value
|
|
// is unmarshalled into the existing option, and replaces the existing option.
|
|
//
|
|
// The value is discarded if it's type cannot be inferred. This behavior just
|
|
// feels "safer", although it should never happen if the correct option set
|
|
// is passed in. The situation where this could occur is if a client and server
|
|
// are on different versions with different options.
|
|
func (optSet *OptionSet) UnmarshalJSON(data []byte) error {
|
|
dec := json.NewDecoder(bytes.NewBuffer(data))
|
|
// Should be a json array, so consume the starting open bracket.
|
|
t, err := dec.Token()
|
|
if err != nil {
|
|
return xerrors.Errorf("read array open bracket: %w", err)
|
|
}
|
|
if t != json.Delim('[') {
|
|
return xerrors.Errorf("expected array open bracket, got %q", t)
|
|
}
|
|
|
|
// As long as json elements exist, consume them. The counter is used for
|
|
// better errors.
|
|
var i int
|
|
OptionSetDecodeLoop:
|
|
for dec.More() {
|
|
var opt Option
|
|
// jValue is a placeholder value that allows us to capture the
|
|
// raw json for the value to attempt to unmarshal later.
|
|
var jValue jsonValue
|
|
opt.Value = &jValue
|
|
err := dec.Decode(&opt)
|
|
if err != nil {
|
|
return xerrors.Errorf("decode %d option: %w", i, err)
|
|
}
|
|
// This counter is used to contextualize errors to show which element of
|
|
// the array we failed to decode. It is only used in the error above, as
|
|
// if the above works, we can instead use the Option.Name which is more
|
|
// descriptive and useful. So increment here for the next decode.
|
|
i++
|
|
|
|
// Try to see if the option already exists in the option set.
|
|
// If it does, just update the existing option.
|
|
for optIndex, have := range *optSet {
|
|
if have.Name == opt.Name {
|
|
if jValue != nil {
|
|
err := json.Unmarshal(jValue, &(*optSet)[optIndex].Value)
|
|
if err != nil {
|
|
return xerrors.Errorf("decode option %q value: %w", have.Name, err)
|
|
}
|
|
// Set the opt's value
|
|
opt.Value = (*optSet)[optIndex].Value
|
|
} else {
|
|
// Hopefully the user passed empty values in the option set. There is no easy way
|
|
// to tell, and if we do not do this, it breaks json.Marshal if we do it again on
|
|
// this new option set.
|
|
opt.Value = (*optSet)[optIndex].Value
|
|
}
|
|
// Override the existing.
|
|
(*optSet)[optIndex] = opt
|
|
// Go to the next option to decode.
|
|
continue OptionSetDecodeLoop
|
|
}
|
|
}
|
|
|
|
// If the option doesn't exist, the value will be discarded.
|
|
// We do this because we cannot infer the type of the value.
|
|
opt.Value = DiscardValue
|
|
*optSet = append(*optSet, opt)
|
|
}
|
|
|
|
t, err = dec.Token()
|
|
if err != nil {
|
|
return xerrors.Errorf("read array close bracket: %w", err)
|
|
}
|
|
if t != json.Delim(']') {
|
|
return xerrors.Errorf("expected array close bracket, got %q", t)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Add adds the given Options to the OptionSet.
|
|
func (optSet *OptionSet) Add(opts ...Option) {
|
|
*optSet = append(*optSet, opts...)
|
|
}
|
|
|
|
// Filter will only return options that match the given filter. (return true)
|
|
func (optSet OptionSet) Filter(filter func(opt Option) bool) OptionSet {
|
|
cpy := make(OptionSet, 0)
|
|
for _, opt := range optSet {
|
|
if filter(opt) {
|
|
cpy = append(cpy, opt)
|
|
}
|
|
}
|
|
return cpy
|
|
}
|
|
|
|
// FlagSet returns a pflag.FlagSet for the OptionSet.
|
|
func (optSet *OptionSet) FlagSet() *pflag.FlagSet {
|
|
if optSet == nil {
|
|
return &pflag.FlagSet{}
|
|
}
|
|
|
|
fs := pflag.NewFlagSet("", pflag.ContinueOnError)
|
|
for _, opt := range *optSet {
|
|
if opt.Flag == "" {
|
|
continue
|
|
}
|
|
var noOptDefValue string
|
|
{
|
|
no, ok := opt.Value.(NoOptDefValuer)
|
|
if ok {
|
|
noOptDefValue = no.NoOptDefValue()
|
|
}
|
|
}
|
|
|
|
val := opt.Value
|
|
if val == nil {
|
|
val = DiscardValue
|
|
}
|
|
|
|
fs.AddFlag(&pflag.Flag{
|
|
Name: opt.Flag,
|
|
Shorthand: opt.FlagShorthand,
|
|
Usage: opt.Description,
|
|
Value: val,
|
|
DefValue: "",
|
|
Changed: false,
|
|
Deprecated: "",
|
|
NoOptDefVal: noOptDefValue,
|
|
Hidden: opt.Hidden,
|
|
})
|
|
}
|
|
fs.Usage = func() {
|
|
_, _ = os.Stderr.WriteString("Override (*FlagSet).Usage() to print help text.\n")
|
|
}
|
|
return fs
|
|
}
|
|
|
|
// ParseEnv parses the given environment variables into the OptionSet.
|
|
// Use EnvsWithPrefix to filter out prefixes.
|
|
func (optSet *OptionSet) ParseEnv(vs []EnvVar) error {
|
|
if optSet == nil {
|
|
return nil
|
|
}
|
|
|
|
var merr *multierror.Error
|
|
|
|
// We parse environment variables first instead of using a nested loop to
|
|
// avoid N*M complexity when there are a lot of options and environment
|
|
// variables.
|
|
envs := make(map[string]string)
|
|
for _, v := range vs {
|
|
envs[v.Name] = v.Value
|
|
}
|
|
|
|
for i, opt := range *optSet {
|
|
if opt.Env == "" {
|
|
continue
|
|
}
|
|
|
|
envVal, ok := envs[opt.Env]
|
|
if !ok {
|
|
// Homebrew strips all environment variables that do not start with `HOMEBREW_`.
|
|
// This prevented using brew to invoke the Coder agent, because the environment
|
|
// variables to not get passed down.
|
|
//
|
|
// A customer wanted to use their custom tap inside a workspace, which was failing
|
|
// because the agent lacked the environment variables to authenticate with Git.
|
|
envVal, ok = envs[`HOMEBREW_`+opt.Env]
|
|
}
|
|
// Currently, empty values are treated as if the environment variable is
|
|
// unset. This behavior is technically not correct as there is now no
|
|
// way for a user to change a Default value to an empty string from
|
|
// the environment. Unfortunately, we have old configuration files
|
|
// that rely on the faulty behavior.
|
|
//
|
|
// TODO: We should remove this hack in May 2023, when deployments
|
|
// have had months to migrate to the new behavior.
|
|
if !ok || envVal == "" {
|
|
continue
|
|
}
|
|
|
|
(*optSet)[i].ValueSource = ValueSourceEnv
|
|
if err := opt.Value.Set(envVal); err != nil {
|
|
merr = multierror.Append(
|
|
merr, xerrors.Errorf("parse %q: %w", opt.Name, err),
|
|
)
|
|
}
|
|
}
|
|
|
|
return merr.ErrorOrNil()
|
|
}
|
|
|
|
// SetDefaults sets the default values for each Option, skipping values
|
|
// that already have a value source.
|
|
func (optSet *OptionSet) SetDefaults() error {
|
|
if optSet == nil {
|
|
return nil
|
|
}
|
|
|
|
var merr *multierror.Error
|
|
|
|
for i, opt := range *optSet {
|
|
// Skip values that may have already been set by the user.
|
|
if opt.ValueSource != ValueSourceNone {
|
|
continue
|
|
}
|
|
|
|
if opt.Default == "" {
|
|
continue
|
|
}
|
|
|
|
if opt.Value == nil {
|
|
merr = multierror.Append(
|
|
merr,
|
|
xerrors.Errorf(
|
|
"parse %q: no Value field set\nFull opt: %+v",
|
|
opt.Name, opt,
|
|
),
|
|
)
|
|
continue
|
|
}
|
|
(*optSet)[i].ValueSource = ValueSourceDefault
|
|
if err := opt.Value.Set(opt.Default); err != nil {
|
|
merr = multierror.Append(
|
|
merr, xerrors.Errorf("parse %q: %w", opt.Name, err),
|
|
)
|
|
}
|
|
}
|
|
return merr.ErrorOrNil()
|
|
}
|
|
|
|
// ByName returns the Option with the given name, or nil if no such option
|
|
// exists.
|
|
func (optSet *OptionSet) ByName(name string) *Option {
|
|
for i := range *optSet {
|
|
opt := &(*optSet)[i]
|
|
if opt.Name == name {
|
|
return opt
|
|
}
|
|
}
|
|
return nil
|
|
}
|