Skip to content

Commit 6106916

Browse files
pstibranypracucci
authored andcommitted
Avoid calling flag.Parse() twice. (#1997)
* Calling flag.Parse() twice confuses some flags. flag.Parse() was called twice previously, first to get -config.file value to parse the config file, and second to parse remaining command line parameters – overwriting values from config file. Unfortunately that confuses command line parameters that accept multiple values. Here we get -config.file via a separate FlagSet to avoid calling flag.Parse() (on default FlagSet) twice. Signed-off-by: Peter Štibraný <[email protected]> * Reverted unintended import order change. Signed-off-by: Peter Štibraný <[email protected]> * Call RegisterFlags(&cfg) to set defaults before parsing config file. Signed-off-by: Peter Štibraný <[email protected]> * Added some unit tests for parsing config. Signed-off-by: Peter Štibraný <[email protected]> * Test mode in main simply dumps the config YAML if no error occurs Added tests for happy path. Signed-off-by: Peter Štibraný <[email protected]> * Separate stdout/stderr expected messages and checks. Signed-off-by: Peter Štibraný <[email protected]> * Use mutexProfileFraction only after flags are parsed. Signed-off-by: Peter Štibraný <[email protected]> * Renamed default test name. Signed-off-by: Peter Štibraný <[email protected]> * Reset testMode to original value at the end of test. Signed-off-by: Peter Štibraný <[email protected]>
1 parent efb54b3 commit 6106916

File tree

6 files changed

+241
-13
lines changed

6 files changed

+241
-13
lines changed

cmd/cortex/main.go

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,36 +25,54 @@ func init() {
2525
prometheus.MustRegister(version.NewCollector("cortex"))
2626
}
2727

28+
const configFileOption = "config.file"
29+
30+
var testMode = false
31+
2832
func main() {
2933
var (
3034
cfg cortex.Config
31-
configFile = ""
3235
eventSampleRate int
3336
ballastBytes int
3437
mutexProfileFraction int
3538
)
36-
flag.StringVar(&configFile, "config.file", "", "Configuration file to load.")
37-
flag.IntVar(&eventSampleRate, "event.sample-rate", 0, "How often to sample observability events (0 = never).")
38-
flag.IntVar(&ballastBytes, "mem-ballast-size-bytes", 0, "Size of memory ballast to allocate.")
39-
flag.IntVar(&mutexProfileFraction, "debug.mutex-profile-fraction", 0, "Fraction at which mutex profile vents will be reported, 0 to disable")
4039

41-
if mutexProfileFraction > 0 {
42-
runtime.SetMutexProfileFraction(mutexProfileFraction)
43-
}
40+
configFile := parseConfigFileParameter()
4441

42+
// This sets default values from flags to the config.
43+
// It needs to be called before parsing the config file!
4544
flagext.RegisterFlags(&cfg)
46-
flag.Parse()
4745

4846
if configFile != "" {
4947
if err := LoadConfig(configFile, &cfg); err != nil {
50-
fmt.Printf("error loading config from %s: %v\n", configFile, err)
48+
fmt.Fprintf(os.Stderr, "error loading config from %s: %v\n", configFile, err)
49+
if testMode {
50+
return
51+
}
5152
os.Exit(1)
5253
}
5354
}
5455

55-
// Parse a second time, as command line flags should take precedent over the config file.
56+
// Ignore -config.file here, since it was already parsed, but it's still present on command line.
57+
flagext.IgnoredFlag(flag.CommandLine, configFileOption, "Configuration file to load.")
58+
flag.IntVar(&eventSampleRate, "event.sample-rate", 0, "How often to sample observability events (0 = never).")
59+
flag.IntVar(&ballastBytes, "mem-ballast-size-bytes", 0, "Size of memory ballast to allocate.")
60+
flag.IntVar(&mutexProfileFraction, "debug.mutex-profile-fraction", 0, "Fraction at which mutex profile vents will be reported, 0 to disable")
61+
62+
if testMode {
63+
// Don't exit on error in test mode. Just parse parameters, dump config and stop.
64+
flag.CommandLine.Init(flag.CommandLine.Name(), flag.ContinueOnError)
65+
flag.Parse()
66+
DumpYaml(&cfg)
67+
return
68+
}
69+
5670
flag.Parse()
5771

72+
if mutexProfileFraction > 0 {
73+
runtime.SetMutexProfileFraction(mutexProfileFraction)
74+
}
75+
5876
// Validate the config once both the config file has been loaded
5977
// and CLI flags parsed.
6078
err := cfg.Validate()
@@ -90,6 +108,18 @@ func main() {
90108
util.CheckFatal("initializing cortex", err)
91109
}
92110

111+
// Parse -config.file option via separate flag set, to avoid polluting default one and calling flag.Parse on it twice.
112+
func parseConfigFileParameter() string {
113+
var configFile = ""
114+
// ignore errors and any output here. Any flag errors will be reported by main flag.Parse() call.
115+
fs := flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
116+
fs.SetOutput(ioutil.Discard)
117+
fs.StringVar(&configFile, configFileOption, "", "") // usage not used in this function.
118+
_ = fs.Parse(os.Args[1:])
119+
120+
return configFile
121+
}
122+
93123
// LoadConfig read YAML-formatted config from filename into cfg.
94124
func LoadConfig(filename string, cfg *cortex.Config) error {
95125
buf, err := ioutil.ReadFile(filename)
@@ -104,3 +134,12 @@ func LoadConfig(filename string, cfg *cortex.Config) error {
104134

105135
return nil
106136
}
137+
138+
func DumpYaml(cfg *cortex.Config) {
139+
out, err := yaml.Marshal(cfg)
140+
if err != nil {
141+
fmt.Fprintln(os.Stderr, err)
142+
} else {
143+
fmt.Printf("%s\n", out)
144+
}
145+
}

cmd/cortex/main_test.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"flag"
6+
"io"
7+
"io/ioutil"
8+
"os"
9+
"sync"
10+
"testing"
11+
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
func TestFlagParsing(t *testing.T) {
16+
for name, tc := range map[string]struct {
17+
arguments []string
18+
yaml string
19+
stdoutMessage string // string that must be included in stdout
20+
stderrMessage string // string that must be included in stderr
21+
}{
22+
"help": {
23+
arguments: []string{"-h"},
24+
stderrMessage: configFileOption,
25+
},
26+
27+
// check that config file is used
28+
"config with unknown target": {
29+
yaml: "target: unknown",
30+
stderrMessage: "unrecognised module name: unknown",
31+
},
32+
33+
"argument with unknown target": {
34+
arguments: []string{"-target=unknown"},
35+
stderrMessage: "unrecognised module name: unknown",
36+
},
37+
38+
"unknown flag": {
39+
arguments: []string{"-unknown.flag"},
40+
stderrMessage: "-unknown.flag",
41+
},
42+
43+
"config with wrong argument override": {
44+
yaml: "target: ingester",
45+
arguments: []string{"-target=unknown"},
46+
stderrMessage: "unrecognised module name: unknown",
47+
},
48+
49+
"default values": {
50+
stdoutMessage: "target: all\n",
51+
},
52+
53+
"config": {
54+
yaml: "target: ingester",
55+
stdoutMessage: "target: ingester\n",
56+
},
57+
58+
"config with arguments override": {
59+
yaml: "target: ingester",
60+
arguments: []string{"-target=distributor"},
61+
stdoutMessage: "target: distributor\n",
62+
},
63+
64+
// we cannot test the happy path, as cortex would then fully start
65+
} {
66+
t.Run(name, func(t *testing.T) {
67+
testSingle(t, tc.arguments, tc.yaml, []byte(tc.stdoutMessage), []byte(tc.stderrMessage))
68+
})
69+
}
70+
}
71+
72+
func testSingle(t *testing.T, arguments []string, yaml string, stdoutMessage, stderrMessage []byte) {
73+
oldArgs, oldStdout, oldStderr, oldTestMode := os.Args, os.Stdout, os.Stderr, testMode
74+
defer func() {
75+
os.Stdout = oldStdout
76+
os.Stderr = oldStderr
77+
os.Args = oldArgs
78+
testMode = oldTestMode
79+
}()
80+
81+
if yaml != "" {
82+
tempFile, err := ioutil.TempFile("", "test")
83+
require.NoError(t, err)
84+
85+
defer func() {
86+
require.NoError(t, tempFile.Close())
87+
require.NoError(t, os.Remove(tempFile.Name()))
88+
}()
89+
90+
_, err = tempFile.WriteString(yaml)
91+
require.NoError(t, err)
92+
93+
arguments = append([]string{"-" + configFileOption, tempFile.Name()}, arguments...)
94+
}
95+
96+
arguments = append([]string{"./cortex"}, arguments...)
97+
98+
testMode = true
99+
os.Args = arguments
100+
co := captureOutput(t)
101+
102+
// reset default flags
103+
flag.CommandLine = flag.NewFlagSet(arguments[0], flag.ExitOnError)
104+
105+
main()
106+
107+
stdout, stderr := co.Done()
108+
if !bytes.Contains(stdout, stdoutMessage) {
109+
t.Errorf("Expected on stdout: %q, stdout: %s\n", stdoutMessage, stdout)
110+
}
111+
if !bytes.Contains(stderr, stderrMessage) {
112+
t.Errorf("Expected on stderr: %q, stderr: %s\n", stderrMessage, stderr)
113+
}
114+
}
115+
116+
type capturedOutput struct {
117+
stdoutBuf bytes.Buffer
118+
stderrBuf bytes.Buffer
119+
120+
wg sync.WaitGroup
121+
stdoutReader, stdoutWriter *os.File
122+
stderrReader, stderrWriter *os.File
123+
}
124+
125+
func captureOutput(t *testing.T) *capturedOutput {
126+
stdoutR, stdoutW, err := os.Pipe()
127+
require.NoError(t, err)
128+
os.Stdout = stdoutW
129+
130+
stderrR, stderrW, err := os.Pipe()
131+
require.NoError(t, err)
132+
os.Stderr = stderrW
133+
134+
co := &capturedOutput{
135+
stdoutReader: stdoutR,
136+
stdoutWriter: stdoutW,
137+
stderrReader: stderrR,
138+
stderrWriter: stderrW,
139+
}
140+
co.wg.Add(1)
141+
go func() {
142+
defer co.wg.Done()
143+
_, _ = io.Copy(&co.stdoutBuf, stdoutR)
144+
}()
145+
146+
co.wg.Add(1)
147+
go func() {
148+
defer co.wg.Done()
149+
_, _ = io.Copy(&co.stderrBuf, stderrR)
150+
}()
151+
152+
return co
153+
}
154+
155+
func (co *capturedOutput) Done() (stdout []byte, stderr []byte) {
156+
// we need to close writers for readers to stop
157+
_ = co.stdoutWriter.Close()
158+
_ = co.stderrWriter.Close()
159+
160+
co.wg.Wait()
161+
162+
return co.stdoutBuf.Bytes(), co.stderrBuf.Bytes()
163+
}

pkg/cortex/modules.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ func (m *moduleName) Set(s string) error {
137137
}
138138
}
139139

140+
func (m moduleName) MarshalYAML() (interface{}, error) {
141+
return m.String(), nil
142+
}
143+
140144
func (m *moduleName) UnmarshalYAML(unmarshal func(interface{}) error) error {
141145
var s string
142146
if err := unmarshal(&s); err != nil {

pkg/ring/kv/multi.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ type MultiConfig struct {
5050
MirrorTimeout time.Duration `yaml:"mirror_timeout"`
5151

5252
// ConfigProvider returns channel with MultiRuntimeConfig updates.
53-
ConfigProvider func() <-chan MultiRuntimeConfig
53+
ConfigProvider func() <-chan MultiRuntimeConfig `yaml:"-"`
5454
}
5555

5656
// RegisterFlagsWithPrefix registers flags with prefix.

pkg/util/flagext/ignored.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package flagext
2+
3+
import (
4+
"flag"
5+
)
6+
7+
type ignoredFlag struct {
8+
name string
9+
}
10+
11+
func (ignoredFlag) String() string {
12+
return "ignored"
13+
}
14+
15+
func (d ignoredFlag) Set(string) error {
16+
return nil
17+
}
18+
19+
// IgnoredFlag ignores set value, without any warning
20+
func IgnoredFlag(f *flag.FlagSet, name, message string) {
21+
f.Var(ignoredFlag{name}, name, message)
22+
}

pkg/util/runtimeconfig/manager.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ type Loader func(filename string) (interface{}, error)
2424
type ManagerConfig struct {
2525
ReloadPeriod time.Duration `yaml:"period"`
2626
LoadPath string `yaml:"file"`
27-
Loader Loader
27+
Loader Loader `yaml:"-"`
2828
}
2929

3030
// RegisterFlags registers flags.

0 commit comments

Comments
 (0)