From 3dcd310a9aabbe15a2a4d9f7176571e13195b3ab Mon Sep 17 00:00:00 2001 From: Dimitri Herzog Date: Sun, 26 May 2024 20:45:30 +0200 Subject: [PATCH] feat: CLI command for configuration validation (#1497) --- cmd/blocking.go | 7 +++--- cmd/cache.go | 5 +++-- cmd/lists.go | 5 +++-- cmd/query.go | 9 ++++---- cmd/root.go | 21 +++++++++--------- cmd/root_test.go | 4 ++-- cmd/serve.go | 10 +++++---- cmd/serve_test.go | 4 ++-- cmd/validate.go | 38 ++++++++++++++++++++++++++++++++ cmd/validate_test.go | 52 ++++++++++++++++++++++++++++++++++++++++++++ docs/interfaces.md | 1 + 11 files changed, 126 insertions(+), 30 deletions(-) create mode 100644 cmd/validate.go create mode 100644 cmd/validate_test.go diff --git a/cmd/blocking.go b/cmd/blocking.go index e77120e31..d83c620cc 100644 --- a/cmd/blocking.go +++ b/cmd/blocking.go @@ -13,9 +13,10 @@ import ( func newBlockingCommand() *cobra.Command { c := &cobra.Command{ - Use: "blocking", - Aliases: []string{"block"}, - Short: "Control status of blocking resolver", + Use: "blocking", + Aliases: []string{"block"}, + Short: "Control status of blocking resolver", + PersistentPreRunE: initConfigPreRun, } c.AddCommand(&cobra.Command{ Use: "enable", diff --git a/cmd/cache.go b/cmd/cache.go index 44d9cbc9c..858c9a325 100644 --- a/cmd/cache.go +++ b/cmd/cache.go @@ -10,8 +10,9 @@ import ( func newCacheCommand() *cobra.Command { c := &cobra.Command{ - Use: "cache", - Short: "Performs cache operations", + Use: "cache", + Short: "Performs cache operations", + PersistentPreRunE: initConfigPreRun, } c.AddCommand(&cobra.Command{ Use: "flush", diff --git a/cmd/lists.go b/cmd/lists.go index badaa8fc7..58e2197b2 100644 --- a/cmd/lists.go +++ b/cmd/lists.go @@ -11,8 +11,9 @@ import ( // NewListsCommand creates new command instance func NewListsCommand() *cobra.Command { c := &cobra.Command{ - Use: "lists", - Short: "lists operations", + Use: "lists", + Short: "lists operations", + PersistentPreRunE: initConfigPreRun, } c.AddCommand(newRefreshCommand()) diff --git a/cmd/query.go b/cmd/query.go index 609372ccd..238497410 100644 --- a/cmd/query.go +++ b/cmd/query.go @@ -14,10 +14,11 @@ import ( // NewQueryCommand creates new command instance func NewQueryCommand() *cobra.Command { c := &cobra.Command{ - Use: "query ", - Args: cobra.ExactArgs(1), - Short: "performs DNS query", - RunE: query, + Use: "query ", + Args: cobra.ExactArgs(1), + Short: "performs DNS query", + RunE: query, + PersistentPreRunE: initConfigPreRun, } c.Flags().StringP("type", "t", "A", "query type (A, AAAA, ...)") diff --git a/cmd/root.go b/cmd/root.go index eb90c287b..bcac1d9b4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -10,8 +10,6 @@ import ( "github.com/0xERR0R/blocky/config" "github.com/0xERR0R/blocky/log" - "github.com/0xERR0R/blocky/util" - "github.com/spf13/cobra" ) @@ -39,6 +37,7 @@ func NewRootCommand() *cobra.Command { and ad-blocker for local network. Complete documentation is available at https://github.com/0xERR0R/blocky`, + PreRunE: initConfigPreRun, RunE: func(cmd *cobra.Command, args []string) error { return newServeCommand().RunE(cmd, args) }, @@ -56,7 +55,8 @@ Complete documentation is available at https://github.com/0xERR0R/blocky`, newBlockingCommand(), NewListsCommand(), NewHealthcheckCommand(), - newCacheCommand()) + newCacheCommand(), + NewValidateCommand()) return c } @@ -65,12 +65,11 @@ func apiURL() string { return fmt.Sprintf("http://%s%s", net.JoinHostPort(apiHost, strconv.Itoa(int(apiPort))), "/api") } -//nolint:gochecknoinits -func init() { - cobra.OnInitialize(initConfig) +func initConfigPreRun(cmd *cobra.Command, args []string) error { + return initConfig() } -func initConfig() { +func initConfig() error { if configPath == defaultConfigPath { val, present := os.LookupEnv(configFileEnvVar) if present { @@ -85,7 +84,7 @@ func initConfig() { cfg, err := config.LoadConfig(configPath, false) if err != nil { - util.FatalOnError("unable to load configuration: ", err) + return fmt.Errorf("unable to load configuration file '%s': %w", configPath, err) } log.Configure(&cfg.Log) @@ -99,13 +98,13 @@ func initConfig() { port, err := config.ConvertPort(split[lastIdx]) if err != nil { - util.FatalOnError("can't convert port to number (1 - 65535)", err) - - return + return fmt.Errorf("can't convert port '%s' to number (1 - 65535): %w", split[lastIdx], err) } apiPort = port } + + return nil } // Execute starts the command diff --git a/cmd/root_test.go b/cmd/root_test.go index b5769a2d0..8f190e3cd 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -53,7 +53,7 @@ var _ = Describe("root command", func() { os.Setenv(configFileEnvVarOld, tmpFile.Path) DeferCleanup(func() { os.Unsetenv(configFileEnvVarOld) }) - initConfig() + Expect(initConfig()).Should(Succeed()) Expect(configPath).Should(Equal(tmpFile.Path)) }) @@ -62,7 +62,7 @@ var _ = Describe("root command", func() { os.Setenv(configFileEnvVar, tmpFile.Path) DeferCleanup(func() { os.Unsetenv(configFileEnvVar) }) - initConfig() + Expect(initConfig()).Should(Succeed()) Expect(configPath).Should(Equal(tmpFile.Path)) }) diff --git a/cmd/serve.go b/cmd/serve.go index 6520e3a4c..c05810dad 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -25,10 +25,12 @@ var ( func newServeCommand() *cobra.Command { return &cobra.Command{ - Use: "serve", - Args: cobra.NoArgs, - Short: "start blocky DNS server (default command)", - RunE: startServer, + Use: "serve", + Args: cobra.NoArgs, + Short: "start blocky DNS server (default command)", + RunE: startServer, + PersistentPreRunE: initConfigPreRun, + SilenceUsage: true, } } diff --git a/cmd/serve_test.go b/cmd/serve_test.go index 7a2fc2eab..301fdb7e1 100644 --- a/cmd/serve_test.go +++ b/cmd/serve_test.go @@ -42,7 +42,7 @@ var _ = Describe("Serve command", func() { os.Setenv(configFileEnvVar, cfgFile.Path) DeferCleanup(func() { os.Unsetenv(configFileEnvVar) }) - initConfig() + Expect(initConfig()).Should(Succeed()) }) errChan := make(chan error) @@ -89,7 +89,7 @@ var _ = Describe("Serve command", func() { os.Setenv(configFileEnvVar, cfgFile.Path) DeferCleanup(func() { os.Unsetenv(configFileEnvVar) }) - initConfig() + Expect(initConfig()).Should(Succeed()) }) errChan := make(chan error) diff --git a/cmd/validate.go b/cmd/validate.go new file mode 100644 index 000000000..e14dd262f --- /dev/null +++ b/cmd/validate.go @@ -0,0 +1,38 @@ +package cmd + +import ( + "errors" + "os" + + "github.com/0xERR0R/blocky/log" + + "github.com/spf13/cobra" +) + +// NewValidateCommand creates new command instance +func NewValidateCommand() *cobra.Command { + return &cobra.Command{ + Use: "validate", + Args: cobra.NoArgs, + Short: "Validates the configuration", + RunE: validateConfiguration, + } +} + +func validateConfiguration(_ *cobra.Command, _ []string) error { + log.Log().Infof("Validating configuration file: %s", configPath) + + _, err := os.Stat(configPath) + if err != nil && errors.Is(err, os.ErrNotExist) { + return errors.New("configuration path does not exist") + } + + err = initConfig() + if err != nil { + return err + } + + log.Log().Info("Configuration is valid") + + return nil +} diff --git a/cmd/validate_test.go b/cmd/validate_test.go new file mode 100644 index 000000000..ec47ebd24 --- /dev/null +++ b/cmd/validate_test.go @@ -0,0 +1,52 @@ +package cmd + +import ( + "github.com/0xERR0R/blocky/helpertest" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Validate command", func() { + var tmpDir *helpertest.TmpFolder + BeforeEach(func() { + tmpDir = helpertest.NewTmpFolder("config") + }) + When("Validate is called with not existing configuration file", func() { + It("should terminate with error", func() { + c := NewRootCommand() + c.SetArgs([]string{"validate", "--config", "/notexisting/path.yaml"}) + + Expect(c.Execute()).Should(HaveOccurred()) + }) + }) + + When("Validate is called with existing valid configuration file", func() { + It("should terminate without error", func() { + cfgFile := tmpDir.CreateStringFile("config.yaml", + "upstreams:", + " groups:", + " default:", + " - 1.1.1.1") + + c := NewRootCommand() + c.SetArgs([]string{"validate", "--config", cfgFile.Path}) + + Expect(c.Execute()).Should(Succeed()) + }) + }) + + When("Validate is called with existing invalid configuration file", func() { + It("should terminate with error", func() { + cfgFile := tmpDir.CreateStringFile("config.yaml", + "upstreams:", + " groups:", + " default:", + " - 1.broken file") + + c := NewRootCommand() + c.SetArgs([]string{"validate", "--config", cfgFile.Path}) + + Expect(c.Execute()).Should(HaveOccurred()) + }) + }) +}) diff --git a/docs/interfaces.md b/docs/interfaces.md index b40ab9bcb..10eb20961 100644 --- a/docs/interfaces.md +++ b/docs/interfaces.md @@ -28,6 +28,7 @@ To run the CLI, please ensure, that blocky DNS server is running, then execute ` - `./blocky query ` execute DNS query (A) (simple replacement for dig, useful for debug purposes) - `./blocky query --type ` execute DNS query with passed query type (A, AAAA, MX, ...) - `./blocky lists refresh` reloads all allow/denylists +- `./blocky validate [--config /path/to/config.yaml]` validates configuration file !!! tip