Skip to content

Commit c4f63ce

Browse files
authored
[+] refactor config command and sub-commands (#621)
* [+] refactor `config` command and sub-commands * [*] remove extra checking now done in `ValidateConfig()` * [+] add `config init` for yaml sources file
1 parent 427bfe2 commit c4f63ce

File tree

3 files changed

+99
-85
lines changed

3 files changed

+99
-85
lines changed

internal/cmdopts/cmdconfig.go

Lines changed: 45 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package cmdopts
33
import (
44
"context"
55
"errors"
6-
"fmt"
76

87
"github.com/cybertec-postgresql/pgwatch/v3/internal/metrics"
98
"github.com/cybertec-postgresql/pgwatch/v3/internal/sources"
@@ -27,91 +26,74 @@ type ConfigInitCommand struct {
2726
owner *Options
2827
}
2928

30-
func (cmd *ConfigInitCommand) Execute([]string) error {
31-
if len(cmd.owner.Sources.Sources)+len(cmd.owner.Metrics.Metrics) == 0 {
32-
return errors.New("both --sources and --metrics are empty")
29+
// Execute initializes the configuration.
30+
func (cmd *ConfigInitCommand) Execute([]string) (err error) {
31+
if err = cmd.owner.ValidateConfig(); err != nil {
32+
return
3333
}
3434
if cmd.owner.Metrics.Metrics > "" {
35-
cmd.InitMetrics()
35+
err = cmd.InitMetrics()
3636
}
3737
if cmd.owner.Sources.Sources > "" && cmd.owner.Metrics.Metrics != cmd.owner.Sources.Sources {
38-
cmd.InitSources()
38+
err = errors.Join(err, cmd.InitSources())
3939
}
40-
cmd.owner.CommandCompleted = true
41-
return nil
40+
cmd.owner.CompleteCommand(map[bool]int32{
41+
true: ExitCodeOK,
42+
false: ExitCodeConfigError,
43+
}[err == nil])
44+
return
4245
}
4346

44-
func (cmd *ConfigInitCommand) InitSources() {
47+
// InitSources initializes the sources configuration.
48+
func (cmd *ConfigInitCommand) InitSources() (err error) {
4549
ctx := context.Background()
46-
kind, err := cmd.owner.GetConfigKind(cmd.owner.Sources.Sources)
47-
if err != nil {
48-
cmd.owner.CompleteCommand(ExitCodeConfigError)
49-
return
50-
}
51-
switch kind {
52-
case ConfigPgURL:
53-
if err = cmd.owner.InitSourceReader(ctx); err != nil {
54-
cmd.owner.CompleteCommand(ExitCodeConfigError)
55-
return
56-
}
57-
case ConfigFile:
58-
writer, _ := sources.NewYAMLSourcesReaderWriter(ctx, cmd.owner.Sources.Sources)
59-
if err = writer.WriteSources(sources.Sources{sources.Source{}}); err != nil {
60-
fmt.Printf("cannot init sources: %s", err)
61-
cmd.owner.CompleteCommand(ExitCodeConfigError)
62-
return
63-
}
50+
opts := cmd.owner
51+
if opts.IsPgConnStr(opts.Sources.Sources) {
52+
return opts.InitSourceReader(ctx)
6453
}
65-
fmt.Println("Sources initialized")
54+
rw, _ := sources.NewYAMLSourcesReaderWriter(ctx, opts.Sources.Sources)
55+
return rw.WriteSources(sources.Sources{sources.Source{}})
6656
}
6757

68-
func (cmd *ConfigInitCommand) InitMetrics() {
58+
// InitMetrics initializes the metrics configuration.
59+
func (cmd *ConfigInitCommand) InitMetrics() (err error) {
6960
ctx := context.Background()
70-
if cmd.owner.IsPgConnStr(cmd.owner.Metrics.Metrics) {
71-
if err := cmd.owner.InitMetricReader(ctx); err != nil {
72-
cmd.owner.CompleteCommand(ExitCodeConfigError)
73-
return
74-
}
75-
} else {
76-
reader, _ := metrics.NewYAMLMetricReaderWriter(ctx, "")
77-
writer, _ := metrics.NewYAMLMetricReaderWriter(ctx, cmd.owner.Metrics.Metrics)
78-
defMetrics, _ := reader.GetMetrics()
79-
if err := writer.WriteMetrics(defMetrics); err != nil {
80-
fmt.Printf("cannot init metrics: %s", err)
81-
cmd.owner.CompleteCommand(ExitCodeConfigError)
82-
return
83-
}
61+
opts := cmd.owner
62+
err = opts.InitMetricReader(ctx)
63+
if err != nil || opts.IsPgConnStr(opts.Metrics.Metrics) {
64+
return // nothing to do, database initialized automatically
8465
}
85-
fmt.Println("Metrics initialized")
66+
reader, _ := metrics.NewYAMLMetricReaderWriter(ctx, "")
67+
defMetrics, _ := reader.GetMetrics()
68+
return opts.MetricsReaderWriter.WriteMetrics(defMetrics)
8669
}
8770

8871
type ConfigUpgradeCommand struct {
8972
owner *Options
9073
}
9174

75+
// Execute upgrades the configuration schema.
9276
func (cmd *ConfigUpgradeCommand) Execute([]string) (err error) {
93-
err = cmd.owner.InitConfigReaders(context.Background())
94-
if err != nil {
95-
cmd.owner.CompleteCommand(ExitCodeConfigError)
96-
fmt.Println(err)
77+
opts := cmd.owner
78+
if err = opts.ValidateConfig(); err != nil {
9779
return
9880
}
99-
if m, ok := cmd.owner.MetricsReaderWriter.(metrics.Migrator); ok {
100-
if err = m.Migrate(); err != nil {
101-
cmd.owner.CompleteCommand(ExitCodeUpgradeError)
102-
fmt.Printf("cannot upgrade metrics: %s", err)
81+
// for now only postgres configuration is upgradable
82+
if opts.IsPgConnStr(opts.Metrics.Metrics) && opts.IsPgConnStr(opts.Sources.Sources) {
83+
err = opts.InitMetricReader(context.Background())
84+
if err != nil {
85+
opts.CompleteCommand(ExitCodeConfigError)
86+
return
87+
}
88+
if m, ok := opts.MetricsReaderWriter.(metrics.Migrator); ok {
89+
err = m.Migrate()
90+
opts.CompleteCommand(map[bool]int32{
91+
true: ExitCodeOK,
92+
false: ExitCodeConfigError,
93+
}[err == nil])
10394
return
10495
}
105-
cmd.owner.CompleteCommand(ExitCodeOK)
106-
fmt.Println("Configuration schema upgraded")
107-
return
10896
}
109-
// if s, ok := cmd.owner.SourcesReaderWriter.(sources.Migrator); ok {
110-
// if err = cmd.UpgradeConfiguration(s); err != nil {
111-
// cmd.owner.CompleteCommand(ExitCodeUpgradeError)
112-
// fmt.Printf("cannot upgrade sources: %s", err)
113-
// return
114-
// }
115-
// }
116-
return errors.New("Configuration storage does not support upgrade")
97+
opts.CompleteCommand(ExitCodeConfigError)
98+
return errors.New("configuration storage does not support upgrade")
11799
}

internal/cmdopts/cmdconfig_test.go

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,49 @@ func TestConfigInitCommand_Execute(t *testing.T) {
4040
a.True(fi.Size() > 0)
4141
})
4242

43+
t.Run("sources is a proper file name", func(*testing.T) {
44+
fname := t.TempDir() + "/sources.yaml"
45+
os.Args = []string{0: "config_test", "--sources=" + fname, "config", "init"}
46+
_, err := New(io.Discard)
47+
a.NoError(err)
48+
a.FileExists(fname)
49+
fi, err := os.Stat(fname)
50+
require.NoError(t, err)
51+
a.True(fi.Size() > 0)
52+
})
53+
4354
t.Run("metrics is an invalid file name", func(*testing.T) {
4455
os.Args = []string{0: "config_test", "--metrics=/", "config", "init"}
4556
opts, err := New(io.Discard)
46-
a.NoError(err) // parsing should not fail
57+
a.Error(err)
4758
a.Equal(ExitCodeConfigError, opts.ExitCode)
4859
})
4960

5061
t.Run("metrics is proper postgres connectin string", func(*testing.T) {
5162
os.Args = []string{0: "config_test", "--metrics=postgresql://foo@bar/baz", "config", "init"}
5263
opts, err := New(io.Discard)
53-
a.NoError(err) // parsing should not fail
64+
a.Error(err)
5465
a.Equal(ExitCodeConfigError, opts.ExitCode)
5566
})
5667

5768
}
69+
70+
func TestConfigUpgradeCommand_Execute(t *testing.T) {
71+
a := assert.New(t)
72+
73+
t.Run("sources and metrics are empty", func(*testing.T) {
74+
os.Args = []string{0: "config_test", "config", "upgrade"}
75+
_, err := New(io.Discard)
76+
a.Error(err)
77+
})
78+
79+
t.Run("metrics is a proper file name but files are not upgradable", func(*testing.T) {
80+
fname := t.TempDir() + "/metrics.yaml"
81+
os.Args = []string{0: "config_test", "--metrics=" + fname, "config", "upgrade"}
82+
c, err := New(io.Discard)
83+
a.Error(err)
84+
a.True(c.CommandCompleted)
85+
a.Equal(ExitCodeConfigError, c.ExitCode)
86+
})
87+
88+
}

internal/cmdopts/cmdoptions.go

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,10 @@ func addCommands(parser *flags.Parser, opts *Options) {
6161
}
6262

6363
// New returns a new instance of Options and immediately executes the subcommand if specified.
64-
// Errors are returned for parsing only, if the command line arguments are invalid.
65-
// Subcommands execution errors (if any) do not affect error returned.
66-
// Subcommands are responsible for setting exit code only.
64+
// Subcommands are responsible for setting exit code.
65+
// Function prints help message only if options are incorrect. If subcommand is executed
66+
// but fails, function outputs the error message only, indicating that some argument
67+
// values might be incorrect, e.g. wrong file name, lack of privileges, etc.
6768
func New(writer io.Writer) (cmdOpts *Options, err error) {
6869
cmdOpts = new(Options)
6970
parser := flags.NewParser(cmdOpts, flags.HelpFlag)
@@ -74,7 +75,7 @@ func New(writer io.Writer) (cmdOpts *Options, err error) {
7475
if flagsErr, ok := err.(*flags.Error); ok && flagsErr.Type == flags.ErrHelp {
7576
cmdOpts.Help = true
7677
}
77-
if !flags.WroteHelp(err) {
78+
if !flags.WroteHelp(err) && !cmdOpts.CommandCompleted {
7879
parser.WriteHelp(writer)
7980
}
8081
return cmdOpts, err
@@ -85,7 +86,7 @@ func New(writer io.Writer) (cmdOpts *Options, err error) {
8586
if len(nonParsedArgs) > 0 { // we don't expect any non-parsed arguments
8687
return cmdOpts, fmt.Errorf("unknown argument(s): %v", nonParsedArgs)
8788
}
88-
err = validateConfig(cmdOpts)
89+
err = cmdOpts.ValidateConfig()
8990
return
9091
}
9192

@@ -123,30 +124,21 @@ func (c *Options) IsPgConnStr(arg string) bool {
123124

124125
// InitMetricReader creates a new source reader based on the configuration kind from the options.
125126
func (c *Options) InitMetricReader(ctx context.Context) (err error) {
126-
if c.Metrics.Metrics == "" { //if config database is configured, use it for metrics as well
127-
if c.IsPgConnStr(c.Sources.Sources) {
128-
c.Metrics.Metrics = c.Sources.Sources
129-
} else { // otherwise use built-in metrics
130-
c.MetricsReaderWriter, err = metrics.NewYAMLMetricReaderWriter(ctx, "")
131-
return err
132-
}
127+
if c.Metrics.Metrics == "" { // use built-in metrics
128+
c.MetricsReaderWriter, err = metrics.NewYAMLMetricReaderWriter(ctx, "")
129+
return
133130
}
134131
if c.IsPgConnStr(c.Metrics.Metrics) {
135132
c.MetricsReaderWriter, err = metrics.NewPostgresMetricReaderWriter(ctx, c.Metrics.Metrics)
136133
} else {
137134
c.MetricsReaderWriter, err = metrics.NewYAMLMetricReaderWriter(ctx, c.Metrics.Metrics)
138135
}
139-
return err
136+
return
140137
}
141138

142139
// InitSourceReader creates a new source reader based on the configuration kind from the options.
143140
func (c *Options) InitSourceReader(ctx context.Context) (err error) {
144141
var configKind Kind
145-
if c.Sources.Sources == "" { //if config database is configured, use it for sources as well
146-
if c.IsPgConnStr(c.Metrics.Metrics) {
147-
c.Sources.Sources = c.Metrics.Metrics
148-
}
149-
}
150142
if configKind, err = c.GetConfigKind(c.Sources.Sources); err != nil {
151143
return
152144
}
@@ -156,7 +148,7 @@ func (c *Options) InitSourceReader(ctx context.Context) (err error) {
156148
default:
157149
c.SourcesReaderWriter, err = sources.NewYAMLSourcesReaderWriter(ctx, c.Sources.Sources)
158150
}
159-
return err
151+
return
160152
}
161153

162154
// InitConfigReaders creates the configuration readers based on the configuration kind from the options.
@@ -178,10 +170,19 @@ func (c *Options) NeedsSchemaUpgrade() (upgrade bool, err error) {
178170
return
179171
}
180172

181-
func validateConfig(c *Options) error {
173+
// ValidateConfig checks if the configuration is valid.
174+
// Configuration database can be specified for one of the --sources or --metrics.
175+
// If one is specified, the other one is set to the same value.
176+
func (c *Options) ValidateConfig() error {
182177
if len(c.Sources.Sources)+len(c.Metrics.Metrics) == 0 {
183178
return errors.New("both --sources and --metrics are empty")
184179
}
180+
switch { // if specified configuration database, use it for both sources and metrics
181+
case c.Sources.Sources == "" && c.IsPgConnStr(c.Metrics.Metrics):
182+
c.Sources.Sources = c.Metrics.Metrics
183+
case c.Metrics.Metrics == "" && c.IsPgConnStr(c.Sources.Sources):
184+
c.Metrics.Metrics = c.Sources.Sources
185+
}
185186
if c.Sources.Refresh <= 1 {
186187
return errors.New("--servers-refresh-loop-seconds must be greater than 1")
187188
}

0 commit comments

Comments
 (0)