Skip to content

Commit 7edea99

Browse files
committed
Add YAML to core clibase parsing
1 parent 64b167a commit 7edea99

File tree

9 files changed

+144
-62
lines changed

9 files changed

+144
-62
lines changed

cli/clibase/cmd.go

Lines changed: 32 additions & 6 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.
@@ -262,17 +263,42 @@ func (inv *Invocation) run(state *runState) error {
262263
parsedArgs = inv.parsedFlags.Args()
263264
}
264265

265-
// Set defaults for flags that weren't set by the user.
266-
skipDefaults := make(map[int]struct{}, len(inv.Command.Options))
266+
// Set value sources for flags.
267267
for i, opt := range inv.Command.Options {
268268
if fl := inv.parsedFlags.Lookup(opt.Flag); fl != nil && fl.Changed {
269-
skipDefaults[i] = struct{}{}
269+
inv.Command.Options[i].ValueSource = ValueSourceFlag
270270
}
271-
if opt.envChanged {
272-
skipDefaults[i] = struct{}{}
271+
}
272+
273+
// Read configs, if any.
274+
for _, opt := range inv.Command.Options {
275+
path, ok := opt.Value.(*YAMLConfigPath)
276+
if !ok || path.String() == "" {
277+
continue
278+
}
279+
280+
fi, err := os.OpenFile(path.String(), os.O_RDONLY, 0)
281+
if err != nil {
282+
return xerrors.Errorf("opening config file: %w", err)
283+
}
284+
//nolint:revive
285+
defer fi.Close()
286+
287+
dec := yaml.NewDecoder(fi)
288+
289+
var n yaml.Node
290+
err = dec.Decode(&n)
291+
if err != nil {
292+
return xerrors.Errorf("decoding config: %w", err)
293+
}
294+
295+
err = inv.Command.Options.FromYAML(&n)
296+
if err != nil {
297+
return xerrors.Errorf("applying config: %w", err)
273298
}
274299
}
275-
err = inv.Command.Options.SetDefaults(skipDefaults)
300+
301+
err = inv.Command.Options.SetDefaults()
276302
if err != nil {
277303
return xerrors.Errorf("setting defaults: %w", err)
278304
}

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: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@ import (
88
"golang.org/x/xerrors"
99
)
1010

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

4858
Hidden bool `json:"hidden,omitempty"`
4959

50-
envChanged bool
60+
ValueSource ValueSource
5161
}
5262

5363
// OptionSet is a group of options that can be applied to a command.
@@ -135,8 +145,7 @@ func (s *OptionSet) ParseEnv(vs []EnvVar) error {
135145
continue
136146
}
137147

138-
opt.envChanged = true
139-
(*s)[i] = opt
148+
(*s)[i].ValueSource = ValueSourceEnv
140149
if err := opt.Value.Set(envVal); err != nil {
141150
merr = multierror.Append(
142151
merr, xerrors.Errorf("parse %q: %w", opt.Name, err),
@@ -148,8 +157,8 @@ func (s *OptionSet) ParseEnv(vs []EnvVar) error {
148157
}
149158

150159
// 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 {
160+
// that already have a value source.
161+
func (s *OptionSet) SetDefaults() error {
153162
if s == nil {
154163
return nil
155164
}
@@ -158,10 +167,8 @@ func (s *OptionSet) SetDefaults(skip map[int]struct{}) error {
158167

159168
for i, opt := range *s {
160169
// 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-
}
170+
if opt.ValueSource != ValueSourceNone {
171+
continue
165172
}
166173

167174
if opt.Default == "" {
@@ -178,6 +185,7 @@ func (s *OptionSet) SetDefaults(skip map[int]struct{}) error {
178185
)
179186
continue
180187
}
188+
(*s)[i].ValueSource = ValueSourceDefault
181189
if err := opt.Value.Set(opt.Default); err != nil {
182190
merr = multierror.Append(
183191
merr, xerrors.Errorf("parse %q: %w", opt.Name, err),

cli/clibase/option_test.go

Lines changed: 2 additions & 2 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_"))

cli/clibase/yaml.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ func fromYAML(os OptionSet, ofGroup *Group, n *yaml.Node) error {
181181
continue
182182
}
183183

184-
if opt.Group.YAML == "" {
184+
if opt.Group != nil && opt.Group.YAML == "" {
185185
return xerrors.Errorf("group yaml name is empty for %q", opt.Name)
186186
}
187187

@@ -213,10 +213,18 @@ func fromYAML(os OptionSet, ofGroup *Group, n *yaml.Node) error {
213213
continue
214214
}
215215

216+
opt, foundOpt := optionsByName[name]
217+
if foundOpt {
218+
if opt.ValueSource != ValueSourceNone {
219+
continue
220+
}
221+
opt.ValueSource = ValueSourceYAML
222+
}
223+
216224
switch item.Kind {
217225
case yaml.MappingNode:
218226
// Item is either a group or an option with a complex object.
219-
if opt, ok := optionsByName[name]; ok {
227+
if foundOpt {
220228
unmarshaler, ok := opt.Value.(yaml.Unmarshaler)
221229
if !ok {
222230
return xerrors.Errorf("complex option %q must support unmarshaling", opt.Name)
@@ -237,8 +245,7 @@ func fromYAML(os OptionSet, ofGroup *Group, n *yaml.Node) error {
237245
}
238246
merr = errors.Join(merr, xerrors.Errorf("unknown option or subgroup %q", name))
239247
case yaml.ScalarNode, yaml.SequenceNode:
240-
opt, ok := optionsByName[name]
241-
if !ok {
248+
if !foundOpt {
242249
merr = errors.Join(merr, xerrors.Errorf("unknown option %q", name))
243250
continue
244251
}

cli/clibase/yaml_test.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,12 @@ func TestOptionSet_YAML(t *testing.T) {
4141
Value: &workspaceName,
4242
Default: "billie",
4343
Description: "The workspace's name.",
44-
Group: &clibase.Group{Name: "Names"},
44+
Group: &clibase.Group{YAML: "names"},
4545
YAML: "workspaceName",
4646
},
4747
}
4848

49-
err := os.SetDefaults(nil)
49+
err := os.SetDefaults()
5050
require.NoError(t, err)
5151

5252
n, err := os.ToYAML()
@@ -139,6 +139,7 @@ func TestOptionSet_YAMLIsomorphism(t *testing.T) {
139139
os2 := slices.Clone(tc.os)
140140
for i := range os2 {
141141
os2[i].Value = tc.zeroValue()
142+
os2[i].ValueSource = clibase.ValueSourceNone
142143
}
143144

144145
// os2 values should be zeroed whereas tc.os should be
@@ -148,6 +149,11 @@ func TestOptionSet_YAMLIsomorphism(t *testing.T) {
148149
err = os2.FromYAML(&y2)
149150
require.NoError(t, err)
150151

152+
want := tc.os
153+
for i := range want {
154+
want[i].ValueSource = clibase.ValueSourceYAML
155+
}
156+
151157
require.Equal(t, tc.os, os2)
152158
})
153159
}

coderd/coderdtest/coderdtest.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1090,7 +1090,7 @@ QastnN77KfUwdj3SJt44U/uh1jAIv4oSLBr8HYUkbnI8
10901090
func DeploymentValues(t *testing.T) *codersdk.DeploymentValues {
10911091
var cfg codersdk.DeploymentValues
10921092
opts := cfg.Options()
1093-
err := opts.SetDefaults(nil)
1093+
err := opts.SetDefaults()
10941094
require.NoError(t, err)
10951095
return &cfg
10961096
}

go.mod

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,6 @@ require (
262262
github.com/hashicorp/terraform-plugin-log v0.7.0 // indirect
263263
github.com/hashicorp/terraform-plugin-sdk/v2 v2.20.0 // indirect
264264
github.com/hdevalence/ed25519consensus v0.0.0-20220222234857-c00d1f31bab3 // indirect
265-
github.com/iancoleman/strcase v0.2.0
266265
github.com/illarion/gonotify v1.0.1 // indirect
267266
github.com/imdario/mergo v0.3.13 // indirect
268267
github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8 // indirect

go.sum

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=
33
bazil.org/fuse v0.0.0-20200407214033-5883e5a4b512/go.mod h1:FbcW6z/2VytnFDhZfumh8Ss8zxHE6qpMP5sHTRe0EaM=
44
bitbucket.org/creachadair/shell v0.0.6/go.mod h1:8Qqi/cYk7vPnsOePHroKXDJYmb5x7ENhtiFtfZq8K+M=
5-
cdr.dev/slog v1.4.2-0.20230228204227-60d22dceaf04 h1:d5MQ+iI2zk7t0HrHwBP9p7k2XfRsXnRclSe8Kpp3xOo=
6-
cdr.dev/slog v1.4.2-0.20230228204227-60d22dceaf04/go.mod h1:YPVZsUbRMaLaPgme0RzlPWlC7fI7YmDj/j/kZLuvICs=
75
cdr.dev/slog v1.4.2 h1:fIfiqASYQFJBZiASwL825atyzeA96NsqSxx2aL61P8I=
86
cdr.dev/slog v1.4.2/go.mod h1:0EkH+GkFNxizNR+GAXUEdUHanxUH5t9zqPILmPM/Vn8=
97
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
@@ -1093,7 +1091,6 @@ github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq
10931091
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
10941092
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis=
10951093
github.com/iancoleman/orderedmap v0.2.0 h1:sq1N/TFpYH++aViPcaKjys3bDClUEU7s5B+z6jq8pNA=
1096-
github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0=
10971094
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
10981095
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
10991096
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=

0 commit comments

Comments
 (0)