github.com/crowdsecurity/crowdsec@v1.6.1/pkg/fflag/features.go (about)

     1  // Package fflag provides a simple feature flag system.
     2  //
     3  // Feature names are lowercase and can only contain letters, numbers, undercores
     4  // and dots.
     5  //
     6  // good: "foo", "foo_bar", "foo.bar"
     7  // bad: "Foo", "foo-bar"
     8  //
     9  // A feature flag can be enabled by the user with an environment variable
    10  // or by adding it to {ConfigDir}/feature.yaml
    11  //
    12  // I.e. CROWDSEC_FEATURE_FOO_BAR=true
    13  // or in feature.yaml:
    14  // ---
    15  // - foo_bar
    16  //
    17  // If the variable is set to false, the feature can still be enabled
    18  // in feature.yaml. Features cannot be disabled in the file.
    19  //
    20  // A feature flag can be deprecated or retired. A deprecated feature flag is
    21  // still accepted but a warning is logged (only if a deprecation message is provided).
    22  // A retired feature flag is ignored and an error is logged.
    23  //
    24  // The message is inteded to inform the user of the behavior
    25  // that has been decided when the flag is/was finally retired.
    26  
    27  package fflag
    28  
    29  import (
    30  	"errors"
    31  	"fmt"
    32  	"io"
    33  	"os"
    34  	"regexp"
    35  	"sort"
    36  	"strings"
    37  
    38  	"github.com/goccy/go-yaml"
    39  	"github.com/sirupsen/logrus"
    40  )
    41  
    42  var (
    43  	ErrFeatureNameEmpty   = errors.New("name is empty")
    44  	ErrFeatureNameCase    = errors.New("name is not lowercase")
    45  	ErrFeatureNameInvalid = errors.New("invalid name (allowed a-z, 0-9, _, .)")
    46  	ErrFeatureUnknown     = errors.New("unknown feature")
    47  	ErrFeatureDeprecated  = errors.New("the flag is deprecated")
    48  	ErrFeatureRetired     = errors.New("the flag is retired")
    49  )
    50  
    51  const (
    52  	ActiveState     = iota // the feature can be enabled, and its description is logged (Info)
    53  	DeprecatedState        // the feature can be enabled, and a deprecation message is logged (Warning)
    54  	RetiredState           // the feature is ignored and a deprecation message is logged (Error)
    55  )
    56  
    57  type Feature struct {
    58  	Name  string
    59  	State int // active, deprecated, retired
    60  
    61  	// Description should be a short sentence, explaining the feature.
    62  	Description string
    63  
    64  	// DeprecationMessage is used to inform the user of the behavior that has
    65  	// been decided when the flag is/was finally retired.
    66  	DeprecationMsg string
    67  
    68  	enabled bool
    69  }
    70  
    71  func (f *Feature) IsEnabled() bool {
    72  	return f.enabled
    73  }
    74  
    75  // Set enables or disables a feature flag
    76  // It should not be called directly by the user, but by SetFromEnv or SetFromYaml
    77  func (f *Feature) Set(value bool) error {
    78  	// retired feature flags are ignored
    79  	if f.State == RetiredState {
    80  		return ErrFeatureRetired
    81  	}
    82  
    83  	f.enabled = value
    84  
    85  	// deprecated feature flags are still accepted, but a warning is triggered.
    86  	// We return an error but set the feature anyway.
    87  	if f.State == DeprecatedState {
    88  		return ErrFeatureDeprecated
    89  	}
    90  
    91  	return nil
    92  }
    93  
    94  // A register allows to enable features from the environment or a file
    95  type FeatureRegister struct {
    96  	EnvPrefix string
    97  	features  map[string]*Feature
    98  }
    99  
   100  var featureNameRexp = regexp.MustCompile(`^[a-z0-9_\.]+$`)
   101  
   102  func validateFeatureName(featureName string) error {
   103  	if featureName == "" {
   104  		return ErrFeatureNameEmpty
   105  	}
   106  
   107  	if featureName != strings.ToLower(featureName) {
   108  		return ErrFeatureNameCase
   109  	}
   110  
   111  	if !featureNameRexp.MatchString(featureName) {
   112  		return ErrFeatureNameInvalid
   113  	}
   114  
   115  	return nil
   116  }
   117  
   118  func (fr *FeatureRegister) RegisterFeature(feat *Feature) error {
   119  	if err := validateFeatureName(feat.Name); err != nil {
   120  		return fmt.Errorf("feature flag '%s': %w", feat.Name, err)
   121  	}
   122  
   123  	if fr.features == nil {
   124  		fr.features = make(map[string]*Feature)
   125  	}
   126  
   127  	fr.features[feat.Name] = feat
   128  
   129  	return nil
   130  }
   131  
   132  func (fr *FeatureRegister) GetFeature(featureName string) (*Feature, error) {
   133  	feat, ok := fr.features[featureName]
   134  	if !ok {
   135  		return feat, ErrFeatureUnknown
   136  	}
   137  
   138  	return feat, nil
   139  }
   140  
   141  func (fr *FeatureRegister) SetFromEnv(logger *logrus.Logger) error {
   142  	for _, e := range os.Environ() {
   143  		// ignore non-feature variables
   144  		if !strings.HasPrefix(e, fr.EnvPrefix) {
   145  			continue
   146  		}
   147  
   148  		// extract feature name and value
   149  		pair := strings.SplitN(e, "=", 2)
   150  		varName := pair[0]
   151  		featureName := strings.ToLower(varName[len(fr.EnvPrefix):])
   152  		value := pair[1]
   153  
   154  		var enable bool
   155  
   156  		switch value {
   157  		case "true":
   158  			enable = true
   159  		case "false":
   160  			enable = false
   161  		default:
   162  			logger.Errorf("Ignored envvar %s=%s: invalid value (must be 'true' or 'false')", varName, value)
   163  			continue
   164  		}
   165  
   166  		feat, err := fr.GetFeature(featureName)
   167  		if err != nil {
   168  			logger.Errorf("Ignored envvar '%s': %s.", varName, err)
   169  			continue
   170  		}
   171  
   172  		err = feat.Set(enable)
   173  
   174  		switch {
   175  		case errors.Is(err, ErrFeatureRetired):
   176  			logger.Errorf("Ignored envvar '%s': %s. %s", varName, err, feat.DeprecationMsg)
   177  			continue
   178  		case errors.Is(err, ErrFeatureDeprecated):
   179  			if feat.DeprecationMsg != "" {
   180  				logger.Warningf("Envvar '%s': %s. %s", varName, err, feat.DeprecationMsg)
   181  			}
   182  		case err != nil:
   183  			return err
   184  		}
   185  
   186  		logger.Debugf("Feature flag: %s=%t (from envvar). %s", featureName, enable, feat.Description)
   187  	}
   188  
   189  	return nil
   190  }
   191  
   192  func (fr *FeatureRegister) SetFromYaml(r io.Reader, logger *logrus.Logger) error {
   193  	var cfg []string
   194  
   195  	bys, err := io.ReadAll(r)
   196  	if err != nil {
   197  		return err
   198  	}
   199  
   200  	// parse config file
   201  	if err := yaml.Unmarshal(bys, &cfg); err != nil {
   202  		if !errors.Is(err, io.EOF) {
   203  			return fmt.Errorf("failed to parse feature flags: %w", err)
   204  		}
   205  
   206  		logger.Debug("No feature flags in config file")
   207  	}
   208  
   209  	// set features
   210  	for _, k := range cfg {
   211  		feat, err := fr.GetFeature(k)
   212  		if err != nil {
   213  			logger.Errorf("Ignored feature flag '%s': %s", k, err)
   214  			continue
   215  		}
   216  
   217  		err = feat.Set(true)
   218  
   219  		switch {
   220  		case errors.Is(err, ErrFeatureRetired):
   221  			logger.Errorf("Ignored feature flag '%s': %s. %s", k, err, feat.DeprecationMsg)
   222  			continue
   223  		case errors.Is(err, ErrFeatureDeprecated):
   224  			logger.Warningf("Feature '%s': %s. %s", k, err, feat.DeprecationMsg)
   225  		case err != nil:
   226  			return err
   227  		}
   228  
   229  		logger.Debugf("Feature flag: %s=true (from config file). %s", k, feat.Description)
   230  	}
   231  
   232  	return nil
   233  }
   234  
   235  func (fr *FeatureRegister) SetFromYamlFile(path string, logger *logrus.Logger) error {
   236  	f, err := os.Open(path)
   237  	if err != nil {
   238  		if os.IsNotExist(err) {
   239  			logger.Tracef("Feature flags config file '%s' does not exist", path)
   240  
   241  			return nil
   242  		}
   243  
   244  		return fmt.Errorf("failed to open feature flags file: %w", err)
   245  	}
   246  	defer f.Close()
   247  
   248  	logger.Debugf("Reading feature flags from %s", path)
   249  
   250  	return fr.SetFromYaml(f, logger)
   251  }
   252  
   253  // GetEnabledFeatures returns the list of features that have been enabled by the user
   254  func (fr *FeatureRegister) GetEnabledFeatures() []string {
   255  	ret := make([]string, 0)
   256  
   257  	for k, feat := range fr.features {
   258  		if feat.IsEnabled() {
   259  			ret = append(ret, k)
   260  		}
   261  	}
   262  
   263  	sort.Strings(ret)
   264  
   265  	return ret
   266  }
   267  
   268  // GetAllFeatures returns a slice of all the known features, ordered by name
   269  func (fr *FeatureRegister) GetAllFeatures() []Feature {
   270  	ret := make([]Feature, len(fr.features))
   271  
   272  	i := 0
   273  	for _, feat := range fr.features {
   274  		ret[i] = *feat
   275  		i++
   276  	}
   277  
   278  	sort.Slice(ret, func(i, j int) bool {
   279  		return ret[i].Name < ret[j].Name
   280  	})
   281  
   282  	return ret
   283  }