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 }