Skip to content

Commit eb01ec6

Browse files
committed
Merge branch 'main' into firstuser
2 parents 60a25b1 + db2bdd1 commit eb01ec6

File tree

138 files changed

+6721
-7316
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

138 files changed

+6721
-7316
lines changed

.golangci.yaml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,6 @@ linters:
215215
- asciicheck
216216
- bidichk
217217
- bodyclose
218-
- deadcode
219218
- dogsled
220219
- errcheck
221220
- errname
@@ -259,4 +258,3 @@ linters:
259258
- typecheck
260259
- unconvert
261260
- unused
262-
- varcheck

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,8 @@ docs/admin/prometheus.md: scripts/metricsdocgen/main.go scripts/metricsdocgen/me
501501
yarn run format:write:only ../docs/admin/prometheus.md
502502

503503
docs/cli.md: scripts/clidocgen/main.go $(GO_SRC_FILES) docs/manifest.json
504-
rm -rf ./docs/cli/*.md
504+
# TODO(@ammario): re-enable server.md once we finish clibase migration.
505+
ls ./docs/cli/*.md | grep -vP "\/coder_server" | xargs rm
505506
BASE_PATH="." go run ./scripts/clidocgen
506507
cd site
507508
yarn run format:write:only ../docs/cli.md ../docs/cli/*.md ../docs/manifest.json

cli/clibase/clibase.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Package clibase offers an all-in-one solution for a highly configurable CLI
2+
// application. Within Coder, we use it for our `server` subcommand, which
3+
// demands more functionality than cobra/viper can offer.
4+
//
5+
// We will extend its usage to the rest of our application, completely replacing
6+
// cobra/viper. It's also a candidate to be broken out into its own open-source
7+
// library, so we avoid deep coupling with Coder concepts.
8+
package clibase
9+
10+
import (
11+
"strings"
12+
13+
"golang.org/x/exp/maps"
14+
)
15+
16+
// Group describes a hierarchy of groups that an option or command belongs to.
17+
type Group struct {
18+
Parent *Group `json:"parent,omitempty"`
19+
Name string `json:"name,omitempty"`
20+
Children []Group `json:"children,omitempty"`
21+
Description string `json:"description,omitempty"`
22+
}
23+
24+
func (g *Group) AddChild(child Group) {
25+
child.Parent = g
26+
g.Children = append(g.Children, child)
27+
}
28+
29+
// Ancestry returns the group and all of its parents, in order.
30+
func (g *Group) Ancestry() []Group {
31+
if g == nil {
32+
return nil
33+
}
34+
35+
groups := []Group{*g}
36+
for p := g.Parent; p != nil; p = p.Parent {
37+
// Prepend to the slice so that the order is correct.
38+
groups = append([]Group{*p}, groups...)
39+
}
40+
return groups
41+
}
42+
43+
func (g *Group) FullName() string {
44+
var names []string
45+
for _, g := range g.Ancestry() {
46+
names = append(names, g.Name)
47+
}
48+
return strings.Join(names, " / ")
49+
}
50+
51+
// Annotations is an arbitrary key-mapping used to extend the Option and Command types.
52+
// Its methods won't panic if the map is nil.
53+
type Annotations map[string]string
54+
55+
// Mark sets a value on the annotations map, creating one
56+
// if it doesn't exist. Mark does not mutate the original and
57+
// returns a copy. It is suitable for chaining.
58+
func (a Annotations) Mark(key string, value string) Annotations {
59+
var aa Annotations
60+
if a != nil {
61+
aa = maps.Clone(a)
62+
} else {
63+
aa = make(Annotations)
64+
}
65+
aa[key] = value
66+
return aa
67+
}
68+
69+
// IsSet returns true if the key is set in the annotations map.
70+
func (a Annotations) IsSet(key string) bool {
71+
if a == nil {
72+
return false
73+
}
74+
_, ok := a[key]
75+
return ok
76+
}
77+
78+
// Get retrieves a key from the map, returning false if the key is not found
79+
// or the map is nil.
80+
func (a Annotations) Get(key string) (string, bool) {
81+
if a == nil {
82+
return "", false
83+
}
84+
v, ok := a[key]
85+
return v, ok
86+
}

cli/clibase/cmd.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package clibase
2+
3+
import "strings"
4+
5+
// Cmd describes an executable command.
6+
type Cmd struct {
7+
// Parent is the direct parent of the command.
8+
Parent *Cmd
9+
// Children is a list of direct descendants.
10+
Children []*Cmd
11+
// Use is provided in form "command [flags] [args...]".
12+
Use string
13+
// Short is a one-line description of the command.
14+
Short string
15+
// Long is a detailed description of the command,
16+
// presented on its help page. It may contain examples.
17+
Long string
18+
Options OptionSet
19+
Annotations Annotations
20+
}
21+
22+
// Name returns the first word in the Use string.
23+
func (c *Cmd) Name() string {
24+
return strings.Split(c.Use, " ")[0]
25+
}
26+
27+
// FullName returns the full invocation name of the command,
28+
// as seen on the command line.
29+
func (c *Cmd) FullName() string {
30+
var names []string
31+
32+
if c.Parent != nil {
33+
names = append(names, c.Parent.FullName())
34+
}
35+
names = append(names, c.Name())
36+
return strings.Join(names, " ")
37+
}
38+
39+
// FullName returns usage of the command, preceded
40+
// by the usage of its parents.
41+
func (c *Cmd) FullUsage() string {
42+
var uses []string
43+
if c.Parent != nil {
44+
uses = append(uses, c.Parent.FullUsage())
45+
}
46+
uses = append(uses, c.Use)
47+
return strings.Join(uses, " ")
48+
}

cli/clibase/env.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package clibase
2+
3+
import "strings"
4+
5+
// name returns the name of the environment variable.
6+
func envName(line string) string {
7+
return strings.ToUpper(
8+
strings.SplitN(line, "=", 2)[0],
9+
)
10+
}
11+
12+
// value returns the value of the environment variable.
13+
func envValue(line string) string {
14+
tokens := strings.SplitN(line, "=", 2)
15+
if len(tokens) < 2 {
16+
return ""
17+
}
18+
return tokens[1]
19+
}
20+
21+
// Var represents a single environment variable of form
22+
// NAME=VALUE.
23+
type EnvVar struct {
24+
Name string
25+
Value string
26+
}
27+
28+
// EnvsWithPrefix returns all environment variables starting with
29+
// prefix without said prefix.
30+
func EnvsWithPrefix(environ []string, prefix string) []EnvVar {
31+
var filtered []EnvVar
32+
for _, line := range environ {
33+
name := envName(line)
34+
if strings.HasPrefix(name, prefix) {
35+
filtered = append(filtered, EnvVar{
36+
Name: strings.TrimPrefix(name, prefix),
37+
Value: envValue(line),
38+
})
39+
}
40+
}
41+
return filtered
42+
}

cli/clibase/env_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package clibase_test
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
7+
"github.com/coder/coder/cli/clibase"
8+
)
9+
10+
func TestFilterNamePrefix(t *testing.T) {
11+
t.Parallel()
12+
type args struct {
13+
environ []string
14+
prefix string
15+
}
16+
tests := []struct {
17+
name string
18+
args args
19+
want []clibase.EnvVar
20+
}{
21+
{"empty", args{[]string{}, "SHIRE"}, nil},
22+
{
23+
"ONE",
24+
args{
25+
[]string{
26+
"SHIRE_BRANDYBUCK=hmm",
27+
},
28+
"SHIRE_",
29+
},
30+
[]clibase.EnvVar{
31+
{Name: "BRANDYBUCK", Value: "hmm"},
32+
},
33+
},
34+
}
35+
for _, tt := range tests {
36+
tt := tt
37+
t.Run(tt.name, func(t *testing.T) {
38+
t.Parallel()
39+
if got := clibase.EnvsWithPrefix(tt.args.environ, tt.args.prefix); !reflect.DeepEqual(got, tt.want) {
40+
t.Errorf("EnvsWithPrefix() = %v, want %v", got, tt.want)
41+
}
42+
})
43+
}
44+
}

cli/clibase/option.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package clibase
2+
3+
import (
4+
"os"
5+
6+
"github.com/hashicorp/go-multierror"
7+
"github.com/spf13/pflag"
8+
"golang.org/x/xerrors"
9+
)
10+
11+
// Option is a configuration option for a CLI application.
12+
type Option struct {
13+
Name string `json:"name,omitempty"`
14+
Description string `json:"description,omitempty"`
15+
16+
// Flag is the long name of the flag used to configure this option. If unset,
17+
// flag configuring is disabled.
18+
Flag string `json:"flag,omitempty"`
19+
// FlagShorthand is the one-character shorthand for the flag. If unset, no
20+
// shorthand is used.
21+
FlagShorthand string `json:"flag_shorthand,omitempty"`
22+
23+
// Env is the environment variable used to configure this option. If unset,
24+
// environment configuring is disabled.
25+
Env string `json:"env,omitempty"`
26+
27+
// YAML is the YAML key used to configure this option. If unset, YAML
28+
// configuring is disabled.
29+
YAML string `json:"yaml,omitempty"`
30+
31+
// Default is parsed into Value if set.
32+
Default string `json:"default,omitempty"`
33+
// Value includes the types listed in values.go.
34+
Value pflag.Value `json:"value,omitempty"`
35+
36+
// Annotations enable extensions to clibase higher up in the stack. It's useful for
37+
// help formatting and documentation generation.
38+
Annotations Annotations `json:"annotations,omitempty"`
39+
40+
// Group is a group hierarchy that helps organize this option in help, configs
41+
// and other documentation.
42+
Group *Group `json:"group,omitempty"`
43+
44+
// UseInstead is a list of options that should be used instead of this one.
45+
// The field is used to generate a deprecation warning.
46+
UseInstead []Option `json:"use_instead,omitempty"`
47+
48+
Hidden bool `json:"hidden,omitempty"`
49+
}
50+
51+
// OptionSet is a group of options that can be applied to a command.
52+
type OptionSet []Option
53+
54+
// Add adds the given Options to the OptionSet.
55+
func (s *OptionSet) Add(opts ...Option) {
56+
*s = append(*s, opts...)
57+
}
58+
59+
// FlagSet returns a pflag.FlagSet for the OptionSet.
60+
func (s *OptionSet) FlagSet() *pflag.FlagSet {
61+
fs := pflag.NewFlagSet("", pflag.ContinueOnError)
62+
for _, opt := range *s {
63+
if opt.Flag == "" {
64+
continue
65+
}
66+
var noOptDefValue string
67+
{
68+
no, ok := opt.Value.(NoOptDefValuer)
69+
if ok {
70+
noOptDefValue = no.NoOptDefValue()
71+
}
72+
}
73+
74+
fs.AddFlag(&pflag.Flag{
75+
Name: opt.Flag,
76+
Shorthand: opt.FlagShorthand,
77+
Usage: opt.Description,
78+
Value: opt.Value,
79+
DefValue: "",
80+
Changed: false,
81+
Deprecated: "",
82+
NoOptDefVal: noOptDefValue,
83+
Hidden: opt.Hidden,
84+
})
85+
}
86+
fs.Usage = func() {
87+
_, _ = os.Stderr.WriteString("Override (*FlagSet).Usage() to print help text.\n")
88+
}
89+
return fs
90+
}
91+
92+
// ParseEnv parses the given environment variables into the OptionSet.
93+
func (s *OptionSet) ParseEnv(globalPrefix string, environ []string) error {
94+
var merr *multierror.Error
95+
96+
// We parse environment variables first instead of using a nested loop to
97+
// avoid N*M complexity when there are a lot of options and environment
98+
// variables.
99+
envs := make(map[string]string)
100+
for _, v := range EnvsWithPrefix(environ, globalPrefix) {
101+
envs[v.Name] = v.Value
102+
}
103+
104+
for _, opt := range *s {
105+
if opt.Env == "" {
106+
continue
107+
}
108+
109+
envVal, ok := envs[opt.Env]
110+
if !ok {
111+
continue
112+
}
113+
114+
if err := opt.Value.Set(envVal); err != nil {
115+
merr = multierror.Append(
116+
merr, xerrors.Errorf("parse %q: %w", opt.Name, err),
117+
)
118+
}
119+
}
120+
121+
return merr.ErrorOrNil()
122+
}
123+
124+
// SetDefaults sets the default values for each Option.
125+
// It should be called before all parsing (e.g. ParseFlags, ParseEnv).
126+
func (s *OptionSet) SetDefaults() error {
127+
var merr *multierror.Error
128+
for _, opt := range *s {
129+
if opt.Default == "" {
130+
continue
131+
}
132+
if opt.Value == nil {
133+
merr = multierror.Append(
134+
merr,
135+
xerrors.Errorf(
136+
"parse %q: no Value field set\nFull opt: %+v",
137+
opt.Name, opt,
138+
),
139+
)
140+
continue
141+
}
142+
if err := opt.Value.Set(opt.Default); err != nil {
143+
merr = multierror.Append(
144+
merr, xerrors.Errorf("parse %q: %w", opt.Name, err),
145+
)
146+
}
147+
}
148+
return merr.ErrorOrNil()
149+
}

0 commit comments

Comments
 (0)