...

Source file src/github.com/cybertec-postgresql/pgwatch/v5/internal/cmdopts/cmdoptions.go

Documentation: github.com/cybertec-postgresql/pgwatch/v5/internal/cmdopts

     1  package cmdopts
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"time"
    10  
    11  	"github.com/cybertec-postgresql/pgwatch/v5/internal/db"
    12  	"github.com/cybertec-postgresql/pgwatch/v5/internal/log"
    13  	"github.com/cybertec-postgresql/pgwatch/v5/internal/metrics"
    14  	"github.com/cybertec-postgresql/pgwatch/v5/internal/sinks"
    15  	"github.com/cybertec-postgresql/pgwatch/v5/internal/sources"
    16  	"github.com/cybertec-postgresql/pgwatch/v5/internal/webserver"
    17  	flags "github.com/jessevdk/go-flags"
    18  )
    19  
    20  const (
    21  	ExitCodeOK int32 = iota
    22  	ExitCodeConfigError
    23  	ExitCodeCmdError
    24  	ExitCodeWebUIError
    25  	ExitCodeUpgradeError
    26  	ExitCodeUserCancel
    27  	ExitCodeShutdownCommand
    28  	ExitCodeFatalError
    29  )
    30  
    31  type Kind int
    32  
    33  const (
    34  	ConfigPgURL Kind = iota
    35  	ConfigFile
    36  	ConfigFolder
    37  	ConfigError
    38  )
    39  
    40  // Options contains the command line options.
    41  type Options struct {
    42  	Sources sources.CmdOpts   `group:"Sources"`
    43  	Metrics metrics.CmdOpts   `group:"Metrics"`
    44  	Sinks   sinks.CmdOpts     `group:"Sinks"`
    45  	Logging log.CmdOpts       `group:"Logging"`
    46  	WebUI   webserver.CmdOpts `group:"WebUI"`
    47  	Help    bool
    48  
    49  	SourcesReaderWriter sources.ReaderWriter
    50  	MetricsReaderWriter metrics.ReaderWriter
    51  	SinksWriter         sinks.Writer
    52  
    53  	ExitCode         int32
    54  	CommandCompleted bool
    55  
    56  	OutputWriter io.Writer
    57  }
    58  
    59  func addCommands(parser *flags.Parser, opts *Options) {
    60  	_, _ = parser.AddCommand("metric", "Manage metrics", "", NewMetricCommand(opts))
    61  	_, _ = parser.AddCommand("source", "Manage sources", "", NewSourceCommand(opts))
    62  	_, _ = parser.AddCommand("config", "Manage configurations", "", NewConfigCommand(opts))
    63  }
    64  
    65  // New returns a new instance of Options and immediately executes the subcommand if specified.
    66  // Subcommands are responsible for setting exit code.
    67  // Function prints help message only if options are incorrect. If subcommand is executed
    68  // but fails, function outputs the error message only, indicating that some argument
    69  // values might be incorrect, e.g. wrong file name, lack of privileges, etc.
    70  func New(writer io.Writer) (cmdOpts *Options, err error) {
    71  	cmdOpts = new(Options)
    72  	parser := flags.NewParser(cmdOpts, flags.HelpFlag)
    73  	parser.SubcommandsOptional = true // if not command specified, start monitoring
    74  	cmdOpts.OutputWriter = writer
    75  	addCommands(parser, cmdOpts)
    76  	nonParsedArgs, err := parser.Parse() // parse and execute subcommand if any
    77  	if err != nil {
    78  		if flagsErr, ok := err.(*flags.Error); ok && flagsErr.Type == flags.ErrHelp {
    79  			cmdOpts.Help = true
    80  		}
    81  		if !flags.WroteHelp(err) && !cmdOpts.CommandCompleted {
    82  			parser.WriteHelp(writer)
    83  		}
    84  		return cmdOpts, err
    85  	}
    86  	if cmdOpts.CommandCompleted { // subcommand executed, nothing to do more
    87  		return
    88  	}
    89  	if len(nonParsedArgs) > 0 { // we don't expect any non-parsed arguments
    90  		return cmdOpts, fmt.Errorf("unknown argument(s): %v", nonParsedArgs)
    91  	}
    92  	err = cmdOpts.ValidateConfig()
    93  	return
    94  }
    95  
    96  func (c *Options) CompleteCommand(code int32) {
    97  	c.CommandCompleted = true
    98  	c.ExitCode = code
    99  }
   100  
   101  // Verbose returns true if the debug log is enabled
   102  func (c *Options) Verbose() bool {
   103  	return c.Logging.LogLevel == "debug"
   104  }
   105  
   106  func (c *Options) GetConfigKind(arg string) (_ Kind, err error) {
   107  	if arg == "" {
   108  		return Kind(ConfigError), errors.New("no configuration provided")
   109  	}
   110  	if c.IsPgConnStr(arg) {
   111  		return Kind(ConfigPgURL), nil
   112  	}
   113  	var fi os.FileInfo
   114  	if fi, err = os.Stat(arg); err == nil {
   115  		if fi.IsDir() {
   116  			return Kind(ConfigFolder), nil
   117  		}
   118  		return Kind(ConfigFile), nil
   119  	}
   120  	return Kind(ConfigError), err
   121  }
   122  
   123  func (c *Options) IsPgConnStr(arg string) bool {
   124  	return db.IsPgConnStr(arg)
   125  }
   126  
   127  // InitMetricReader creates a new source reader based on the configuration kind from the options.
   128  func (c *Options) InitMetricReader(ctx context.Context) (err error) {
   129  	if c.Metrics.Metrics == "" { // use built-in metrics
   130  		c.MetricsReaderWriter, err = metrics.NewYAMLMetricReaderWriter(ctx, "")
   131  		return
   132  	}
   133  	if c.IsPgConnStr(c.Metrics.Metrics) {
   134  		c.MetricsReaderWriter, err = metrics.NewPostgresMetricReaderWriter(ctx, c.Metrics.Metrics)
   135  	} else {
   136  		c.MetricsReaderWriter, err = metrics.NewYAMLMetricReaderWriter(ctx, c.Metrics.Metrics)
   137  	}
   138  	return
   139  }
   140  
   141  // InitSourceReader creates a new source reader based on the configuration kind from the options.
   142  func (c *Options) InitSourceReader(ctx context.Context) (err error) {
   143  	var configKind Kind
   144  	if configKind, err = c.GetConfigKind(c.Sources.Sources); err != nil {
   145  		return
   146  	}
   147  	switch configKind {
   148  	case ConfigPgURL:
   149  		c.SourcesReaderWriter, err = sources.NewPostgresSourcesReaderWriter(ctx, c.Sources.Sources)
   150  	default:
   151  		c.SourcesReaderWriter, err = sources.NewYAMLSourcesReaderWriter(ctx, c.Sources.Sources)
   152  	}
   153  	return
   154  }
   155  
   156  // InitConfigReaders creates the configuration readers based on the configuration kind from the options.
   157  func (c *Options) InitConfigReaders(ctx context.Context) error {
   158  	err := errors.Join(c.InitMetricReader(ctx), c.InitSourceReader(ctx))
   159  	if err != nil {
   160  		return err
   161  	}
   162  	return db.NeedsMigration(c.MetricsReaderWriter, metrics.ErrNeedsMigration)
   163  }
   164  
   165  // InitSinkWriter creates a new MultiWriter instance if needed.
   166  func (c *Options) InitSinkWriter(ctx context.Context) (err error) {
   167  	c.SinksWriter, err = sinks.NewSinkWriter(ctx, &c.Sinks)
   168  	if err != nil {
   169  		return err
   170  	}
   171  	return db.NeedsMigration(c.SinksWriter, sinks.ErrNeedsMigration)
   172  }
   173  
   174  // NeedsSchemaUpgrade checks if the configuration database schema needs an upgrade.
   175  func (c *Options) NeedsSchemaUpgrade() (upgrade bool, err error) {
   176  	if m, ok := c.SourcesReaderWriter.(db.Migrator); ok {
   177  		upgrade, err = m.NeedsMigration()
   178  	}
   179  	if upgrade || err != nil {
   180  		return
   181  	}
   182  	if m, ok := c.MetricsReaderWriter.(db.Migrator); ok {
   183  		upgrade, err = m.NeedsMigration()
   184  	}
   185  	if upgrade || err != nil {
   186  		return
   187  	}
   188  	if m, ok := c.SinksWriter.(db.Migrator); ok {
   189  		return m.NeedsMigration()
   190  	}
   191  	return
   192  }
   193  
   194  // ValidateConfig checks if the configuration is valid.
   195  // Configuration database can be specified for one of the --sources or --metrics.
   196  // If one is specified, the other one is set to the same value.
   197  func (c *Options) ValidateConfig() error {
   198  	if len(c.Sources.Sources)+len(c.Metrics.Metrics) == 0 {
   199  		return errors.New("both --sources and --metrics are empty")
   200  	}
   201  	switch { // if specified configuration database, use it for both sources and metrics
   202  	case c.Sources.Sources == "" && c.IsPgConnStr(c.Metrics.Metrics):
   203  		c.Sources.Sources = c.Metrics.Metrics
   204  	case c.Metrics.Metrics == "" && c.IsPgConnStr(c.Sources.Sources):
   205  		c.Metrics.Metrics = c.Sources.Sources
   206  	}
   207  	if c.Sources.Refresh <= 1 {
   208  		return errors.New("--refresh must be greater than 1")
   209  	}
   210  	if c.Sources.MaxParallelConnectionsPerDb < 1 {
   211  		return errors.New("--max-parallel-connections-per-db must be >= 1")
   212  	}
   213  
   214  	// validate that input is boolean is set
   215  	if c.Sinks.BatchingDelay <= 0 || c.Sinks.BatchingDelay > time.Hour {
   216  		return errors.New("--batching-delay must be between 0 and 1h")
   217  	}
   218  
   219  	return nil
   220  }
   221