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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 4 additions & 9 deletions
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

Lines changed: 34 additions & 10 deletions
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

Lines changed: 73 additions & 34 deletions
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

Lines changed: 41 additions & 9 deletions
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

Lines changed: 4 additions & 4 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy