github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/pkg/cli/command.go (about)

     1  package cli
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"regexp"
     9  	"strings"
    10  
    11  	"github.com/spf13/cobra"
    12  	"github.com/spf13/pflag"
    13  	"github.com/spf13/viper"
    14  
    15  	"github.com/pyroscope-io/pyroscope/pkg/util/slices"
    16  )
    17  
    18  // https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html#tag_12_02
    19  const OptionsEnd = "--"
    20  
    21  type CmdRunFn func(cmd *cobra.Command, args []string) error
    22  
    23  func CreateCmdRunFn(cfg interface{}, vpr *viper.Viper, fn CmdRunFn) CmdRunFn {
    24  	return func(cmd *cobra.Command, args []string) error {
    25  		var err error
    26  		var xargs []string
    27  
    28  		args, xargs = splitArgs(cmd.Flags(), args)
    29  		if slices.StringContains(xargs, "--help") {
    30  			_ = cmd.Help()
    31  			return nil
    32  		}
    33  
    34  		if err = vpr.BindPFlags(cmd.Flags()); err != nil {
    35  			return err
    36  		}
    37  
    38  		// Here's the correct order for configuration precedence:
    39  		// * command line arguments
    40  		// * environment variables
    41  		// * config file
    42  		// * defaults
    43  		// also documented here: https://pyroscope.io/docs/server-configuration
    44  
    45  		// Parsing arguments for the first time.
    46  		// The only reason we do this here is so that if you provide -config argument we use the right config path
    47  		if err = cmd.Flags().Parse(xargs); err != nil {
    48  			return err
    49  		}
    50  
    51  		// Read configuration from file, if applicable.
    52  		if err = loadConfigFile(cmd, vpr); err != nil {
    53  			return err
    54  		}
    55  		// Viper deals with both environment variable mappings as well as config files.
    56  		// That's why this is not included in the previous if statement
    57  		if err = Unmarshal(vpr, cfg); err != nil {
    58  			return err
    59  		}
    60  
    61  		// Parsing arguments one more time to override anything set in environment variables or config file
    62  		if err = cmd.Flags().Parse(xargs); err != nil {
    63  			return err
    64  		}
    65  
    66  		if err = fn(cmd, args); err != nil {
    67  			cmd.SilenceUsage = true
    68  		}
    69  		return err
    70  	}
    71  }
    72  
    73  func NewViper(prefix string) *viper.Viper {
    74  	v := viper.New()
    75  	v.SetEnvPrefix(prefix)
    76  	v.SetConfigType("yaml")
    77  	v.AutomaticEnv()
    78  	v.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_"))
    79  	return v
    80  }
    81  
    82  // splitArgs splits raw arguments into
    83  func splitArgs(flags *pflag.FlagSet, args []string) ([]string, []string) {
    84  	var xargs []string
    85  	x := firstArgumentIndex(flags, args)
    86  	if x >= 0 {
    87  		xargs = args[:x]
    88  		args = args[x:]
    89  	} else {
    90  		xargs = args
    91  		args = nil
    92  	}
    93  	return args, prependDash(xargs)
    94  }
    95  
    96  func prependDash1(arg string) string {
    97  	if len(arg) > 2 && strings.HasPrefix(arg, "-") && !strings.HasPrefix(arg, "--") {
    98  		return "-" + arg
    99  	}
   100  	return arg
   101  }
   102  
   103  func prependDash(args []string) []string {
   104  	for i, arg := range args {
   105  		args[i] = prependDash1(arg)
   106  	}
   107  	return args
   108  }
   109  
   110  // firstArgumentIndex returns index of the first encountered argument.
   111  // If args does not contain arguments, or contains undefined flags,
   112  // the call returns -1.
   113  func firstArgumentIndex(flags *pflag.FlagSet, args []string) int {
   114  	for i := 0; i < len(args); i++ {
   115  		a := prependDash1(args[i])
   116  		switch {
   117  		default:
   118  			return i
   119  		case a == OptionsEnd:
   120  			return i + 1
   121  		case strings.HasPrefix(a, OptionsEnd) && len(a) > 2:
   122  			x := strings.SplitN(a[2:], "=", 2)
   123  			f := flags.Lookup(x[0])
   124  			if f == nil {
   125  				return -1
   126  			}
   127  			if f.Value.Type() == "bool" {
   128  				continue
   129  			}
   130  			if len(x) == 1 {
   131  				i++
   132  			}
   133  		}
   134  	}
   135  	// Should have returned earlier.
   136  	return -1
   137  }
   138  
   139  func loadConfigFile(cmd *cobra.Command, vpr *viper.Viper) error {
   140  	cf := cmd.Flags().Lookup("config")
   141  	if cf == nil {
   142  		return nil
   143  	}
   144  	var configPath string
   145  	configPath = cf.Value.String()
   146  	// Note that Changed is set to true even if the specified flag value
   147  	// is equal to the default one. For backward compatibility we only
   148  	// consider an option as user-defined, if its value is different;
   149  	// which may be unexpected.
   150  	userDefined := cf.Changed && configPath != cf.DefValue
   151  	// If configuration file path is overridden with the environment variable
   152  	// and the flag is not specified, read config by the path from the env var.
   153  	if !userDefined {
   154  		if v := os.Getenv("PYROSCOPE_CONFIG"); v != "" {
   155  			configPath = v
   156  			userDefined = true
   157  		}
   158  	}
   159  	if configPath == "" {
   160  		// Must never happen.
   161  		return nil
   162  	}
   163  	vpr.SetConfigFile(configPath)
   164  	data, err := ioutil.ReadFile(configPath)
   165  	if err != nil && errors.Is(err, os.ErrNotExist) && !userDefined {
   166  		// If user did not specify the config file, and the file does not exist,
   167  		//   this is okay
   168  		return nil
   169  	}
   170  
   171  	if err == nil {
   172  		return vpr.ReadConfig(strings.NewReader(performSubstitutions(data)))
   173  	}
   174  
   175  	return fmt.Errorf("loading configuration file: %w", err)
   176  }
   177  
   178  var digitCheck = regexp.MustCompile(`^[0-9]`)
   179  
   180  func performSubstitutions(data []byte) string {
   181  	// return string(data)
   182  	return os.Expand(string(data), func(name string) string {
   183  		// this is here so that $1, $2, etc. work in the config file
   184  		if digitCheck.MatchString(name) {
   185  			return "$" + name
   186  		}
   187  		s := os.Getenv(name)
   188  		return s
   189  	})
   190  }