Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 4b99e2d

Browse files
authored
feat: add YAML support to server (coder#6934)
1 parent a3c6cb1 commit 4b99e2d

32 files changed

+1578
-441
lines changed

.golangci.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ issues:
194194
linters:
195195
# We use assertions rather than explicitly checking errors in tests
196196
- errcheck
197+
- forcetypeassert
197198

198199
fix: true
199200
max-issues-per-linter: 0

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,7 @@ update-golden-files: cli/testdata/.gen-golden helm/tests/testdata/.gen-golden sc
518518
.PHONY: update-golden-files
519519

520520
cli/testdata/.gen-golden: $(wildcard cli/testdata/*.golden) $(wildcard cli/*.tpl) $(GO_SRC_FILES)
521-
go test ./cli -run=TestCommandHelp -update
521+
go test ./cli -run="Test(CommandHelp|ServerYAML)" -update
522522
touch "$@"
523523

524524
helm/tests/testdata/.gen-golden: $(wildcard helm/tests/testdata/*.golden) $(GO_SRC_FILES)

cli/clibase/clibase.go

+4-9
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,10 @@ import (
1414

1515
// Group describes a hierarchy of groups that an option or command belongs to.
1616
type Group struct {
17-
Parent *Group `json:"parent,omitempty"`
18-
Name string `json:"name,omitempty"`
19-
Children []Group `json:"children,omitempty"`
20-
Description string `json:"description,omitempty"`
21-
}
22-
23-
func (g *Group) AddChild(child Group) {
24-
child.Parent = g
25-
g.Children = append(g.Children, child)
17+
Parent *Group `json:"parent,omitempty"`
18+
Name string `json:"name,omitempty"`
19+
YAML string `json:"yaml,omitempty"`
20+
Description string `json:"description,omitempty"`
2621
}
2722

2823
// Ancestry returns the group and all of its parents, in order.

cli/clibase/cmd.go

+34-10
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/spf13/pflag"
1414
"golang.org/x/exp/slices"
1515
"golang.org/x/xerrors"
16+
"gopkg.in/yaml.v3"
1617
)
1718

1819
// Cmd describes an executable command.
@@ -76,10 +77,8 @@ func (c *Cmd) PrepareAll() error {
7677
}
7778
var merr error
7879

79-
slices.SortFunc(c.Options, func(a, b Option) bool {
80-
return a.Flag < b.Flag
81-
})
82-
for _, opt := range c.Options {
80+
for i := range c.Options {
81+
opt := &c.Options[i]
8382
if opt.Name == "" {
8483
switch {
8584
case opt.Flag != "":
@@ -102,6 +101,10 @@ func (c *Cmd) PrepareAll() error {
102101
}
103102
}
104103
}
104+
105+
slices.SortFunc(c.Options, func(a, b Option) bool {
106+
return a.Name < b.Name
107+
})
105108
slices.SortFunc(c.Children, func(a, b *Cmd) bool {
106109
return a.Name() < b.Name()
107110
})
@@ -262,17 +265,38 @@ func (inv *Invocation) run(state *runState) error {
262265
parsedArgs = inv.parsedFlags.Args()
263266
}
264267

265-
// Set defaults for flags that weren't set by the user.
266-
skipDefaults := make(map[int]struct{}, len(inv.Command.Options))
268+
// Set value sources for flags.
267269
for i, opt := range inv.Command.Options {
268270
if fl := inv.parsedFlags.Lookup(opt.Flag); fl != nil && fl.Changed {
269-
skipDefaults[i] = struct{}{}
271+
inv.Command.Options[i].ValueSource = ValueSourceFlag
272+
}
273+
}
274+
275+
// Read YAML configs, if any.
276+
for _, opt := range inv.Command.Options {
277+
path, ok := opt.Value.(*YAMLConfigPath)
278+
if !ok || path.String() == "" {
279+
continue
270280
}
271-
if opt.envChanged {
272-
skipDefaults[i] = struct{}{}
281+
282+
byt, err := os.ReadFile(path.String())
283+
if err != nil {
284+
return xerrors.Errorf("reading yaml: %w", err)
285+
}
286+
287+
var n yaml.Node
288+
err = yaml.Unmarshal(byt, &n)
289+
if err != nil {
290+
return xerrors.Errorf("decoding yaml: %w", err)
291+
}
292+
293+
err = inv.Command.Options.UnmarshalYAML(&n)
294+
if err != nil {
295+
return xerrors.Errorf("applying yaml: %w", err)
273296
}
274297
}
275-
err = inv.Command.Options.SetDefaults(skipDefaults)
298+
299+
err = inv.Command.Options.SetDefaults()
276300
if err != nil {
277301
return xerrors.Errorf("setting defaults: %w", err)
278302
}

cli/clibase/cmd_test.go

+73-34
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"context"
66
"fmt"
7+
"os"
78
"strings"
89
"testing"
910

@@ -555,42 +556,80 @@ func TestCommand_EmptySlice(t *testing.T) {
555556
func TestCommand_DefaultsOverride(t *testing.T) {
556557
t.Parallel()
557558

558-
var got string
559-
cmd := &clibase.Cmd{
560-
Options: clibase.OptionSet{
561-
{
562-
Name: "url",
563-
Flag: "url",
564-
Default: "def.com",
565-
Env: "URL",
566-
Value: clibase.StringOf(&got),
567-
},
568-
},
569-
Handler: (func(i *clibase.Invocation) error {
570-
_, _ = fmt.Fprintf(i.Stdout, "%s", got)
571-
return nil
572-
}),
559+
test := func(name string, want string, fn func(t *testing.T, inv *clibase.Invocation)) {
560+
t.Run(name, func(t *testing.T) {
561+
t.Parallel()
562+
563+
var (
564+
got string
565+
config clibase.YAMLConfigPath
566+
)
567+
cmd := &clibase.Cmd{
568+
Options: clibase.OptionSet{
569+
{
570+
Name: "url",
571+
Flag: "url",
572+
Default: "def.com",
573+
Env: "URL",
574+
Value: clibase.StringOf(&got),
575+
YAML: "url",
576+
},
577+
{
578+
Name: "config",
579+
Flag: "config",
580+
Default: "",
581+
Value: &config,
582+
},
583+
},
584+
Handler: (func(i *clibase.Invocation) error {
585+
_, _ = fmt.Fprintf(i.Stdout, "%s", got)
586+
return nil
587+
}),
588+
}
589+
590+
inv := cmd.Invoke()
591+
stdio := fakeIO(inv)
592+
fn(t, inv)
593+
err := inv.Run()
594+
require.NoError(t, err)
595+
require.Equal(t, want, stdio.Stdout.String())
596+
})
573597
}
574598

575-
// Base case
576-
inv := cmd.Invoke()
577-
stdio := fakeIO(inv)
578-
err := inv.Run()
579-
require.NoError(t, err)
580-
require.Equal(t, "def.com", stdio.Stdout.String())
599+
test("DefaultOverNothing", "def.com", func(t *testing.T, inv *clibase.Invocation) {})
581600

582-
// Flag overrides
583-
inv = cmd.Invoke("--url", "good.com")
584-
stdio = fakeIO(inv)
585-
err = inv.Run()
586-
require.NoError(t, err)
587-
require.Equal(t, "good.com", stdio.Stdout.String())
601+
test("FlagOverDefault", "good.com", func(t *testing.T, inv *clibase.Invocation) {
602+
inv.Args = []string{"--url", "good.com"}
603+
})
588604

589-
// Env overrides
590-
inv = cmd.Invoke()
591-
inv.Environ.Set("URL", "good.com")
592-
stdio = fakeIO(inv)
593-
err = inv.Run()
594-
require.NoError(t, err)
595-
require.Equal(t, "good.com", stdio.Stdout.String())
605+
test("EnvOverDefault", "good.com", func(t *testing.T, inv *clibase.Invocation) {
606+
inv.Environ.Set("URL", "good.com")
607+
})
608+
609+
test("FlagOverEnv", "good.com", func(t *testing.T, inv *clibase.Invocation) {
610+
inv.Environ.Set("URL", "bad.com")
611+
inv.Args = []string{"--url", "good.com"}
612+
})
613+
614+
test("FlagOverYAML", "good.com", func(t *testing.T, inv *clibase.Invocation) {
615+
fi, err := os.CreateTemp(t.TempDir(), "config.yaml")
616+
require.NoError(t, err)
617+
defer fi.Close()
618+
619+
_, err = fi.WriteString("url: bad.com")
620+
require.NoError(t, err)
621+
622+
inv.Args = []string{"--config", fi.Name(), "--url", "good.com"}
623+
})
624+
625+
test("YAMLOverDefault", "good.com", func(t *testing.T, inv *clibase.Invocation) {
626+
fi, err := os.CreateTemp(t.TempDir(), "config.yaml")
627+
require.NoError(t, err)
628+
defer fi.Close()
629+
630+
_, err = fi.WriteString("url: good.com")
631+
require.NoError(t, err)
632+
633+
inv.Args = []string{"--config", fi.Name()}
634+
})
596635
}

cli/clibase/option.go

+41-9
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,23 @@ package clibase
22

33
import (
44
"os"
5+
"strings"
56

67
"github.com/hashicorp/go-multierror"
78
"github.com/spf13/pflag"
89
"golang.org/x/xerrors"
910
)
1011

12+
type ValueSource string
13+
14+
const (
15+
ValueSourceNone ValueSource = ""
16+
ValueSourceFlag ValueSource = "flag"
17+
ValueSourceEnv ValueSource = "env"
18+
ValueSourceYAML ValueSource = "yaml"
19+
ValueSourceDefault ValueSource = "default"
20+
)
21+
1122
// Option is a configuration option for a CLI application.
1223
type Option struct {
1324
Name string `json:"name,omitempty"`
@@ -47,7 +58,18 @@ type Option struct {
4758

4859
Hidden bool `json:"hidden,omitempty"`
4960

50-
envChanged bool
61+
ValueSource ValueSource `json:"value_source,omitempty"`
62+
}
63+
64+
func (o Option) YAMLPath() string {
65+
if o.YAML == "" {
66+
return ""
67+
}
68+
var gs []string
69+
for _, g := range o.Group.Ancestry() {
70+
gs = append(gs, g.YAML)
71+
}
72+
return strings.Join(append(gs, o.YAML), ".")
5173
}
5274

5375
// OptionSet is a group of options that can be applied to a command.
@@ -135,8 +157,7 @@ func (s *OptionSet) ParseEnv(vs []EnvVar) error {
135157
continue
136158
}
137159

138-
opt.envChanged = true
139-
(*s)[i] = opt
160+
(*s)[i].ValueSource = ValueSourceEnv
140161
if err := opt.Value.Set(envVal); err != nil {
141162
merr = multierror.Append(
142163
merr, xerrors.Errorf("parse %q: %w", opt.Name, err),
@@ -148,8 +169,8 @@ func (s *OptionSet) ParseEnv(vs []EnvVar) error {
148169
}
149170

150171
// SetDefaults sets the default values for each Option, skipping values
151-
// that have already been set as indicated by the skip map.
152-
func (s *OptionSet) SetDefaults(skip map[int]struct{}) error {
172+
// that already have a value source.
173+
func (s *OptionSet) SetDefaults() error {
153174
if s == nil {
154175
return nil
155176
}
@@ -158,10 +179,8 @@ func (s *OptionSet) SetDefaults(skip map[int]struct{}) error {
158179

159180
for i, opt := range *s {
160181
// Skip values that may have already been set by the user.
161-
if len(skip) > 0 {
162-
if _, ok := skip[i]; ok {
163-
continue
164-
}
182+
if opt.ValueSource != ValueSourceNone {
183+
continue
165184
}
166185

167186
if opt.Default == "" {
@@ -178,6 +197,7 @@ func (s *OptionSet) SetDefaults(skip map[int]struct{}) error {
178197
)
179198
continue
180199
}
200+
(*s)[i].ValueSource = ValueSourceDefault
181201
if err := opt.Value.Set(opt.Default); err != nil {
182202
merr = multierror.Append(
183203
merr, xerrors.Errorf("parse %q: %w", opt.Name, err),
@@ -186,3 +206,15 @@ func (s *OptionSet) SetDefaults(skip map[int]struct{}) error {
186206
}
187207
return merr.ErrorOrNil()
188208
}
209+
210+
// ByName returns the Option with the given name, or nil if no such option
211+
// exists.
212+
func (s *OptionSet) ByName(name string) *Option {
213+
for i := range *s {
214+
opt := &(*s)[i]
215+
if opt.Name == name {
216+
return opt
217+
}
218+
}
219+
return nil
220+
}

cli/clibase/option_test.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func TestOptionSet_ParseFlags(t *testing.T) {
4949
},
5050
}
5151

52-
err := os.SetDefaults(nil)
52+
err := os.SetDefaults()
5353
require.NoError(t, err)
5454

5555
err = os.FlagSet().Parse([]string{"--name", "foo", "--name", "bar"})
@@ -111,7 +111,7 @@ func TestOptionSet_ParseEnv(t *testing.T) {
111111
},
112112
}
113113

114-
err := os.SetDefaults(nil)
114+
err := os.SetDefaults()
115115
require.NoError(t, err)
116116

117117
err = os.ParseEnv(clibase.ParseEnviron([]string{"CODER_WORKSPACE_NAME="}, "CODER_"))
@@ -133,7 +133,7 @@ func TestOptionSet_ParseEnv(t *testing.T) {
133133
},
134134
}
135135

136-
err := os.SetDefaults(nil)
136+
err := os.SetDefaults()
137137
require.NoError(t, err)
138138

139139
err = os.ParseEnv([]clibase.EnvVar{
@@ -157,7 +157,7 @@ func TestOptionSet_ParseEnv(t *testing.T) {
157157
},
158158
}
159159

160-
err := os.SetDefaults(nil)
160+
err := os.SetDefaults()
161161
require.NoError(t, err)
162162

163163
err = os.ParseEnv([]clibase.EnvVar{

0 commit comments

Comments
 (0)