features: add basic feature flag implementation

This commit is contained in:
Michael Eischer 2024-01-28 16:15:32 +01:00
parent 0589da60b3
commit 5974a79497
4 changed files with 235 additions and 0 deletions

View File

@ -0,0 +1,51 @@
package main
import (
"fmt"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/feature"
"github.com/restic/restic/internal/ui/table"
"github.com/spf13/cobra"
)
// FIXME explain semantics
var featuresCmd = &cobra.Command{
Use: "features",
Short: "Print list of feature flags",
Long: `
The "features" command prints a list of supported feature flags.
EXIT STATUS
===========
Exit status is 0 if the command was successful, and non-zero if there was any error.
`,
Hidden: true,
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, args []string) error {
if len(args) != 0 {
return errors.Fatal("the feature command expects no arguments")
}
fmt.Printf("All Feature Flags:\n")
flags := feature.Flag.List()
tab := table.New()
tab.AddColumn("Name", "{{ .Name }}")
tab.AddColumn("Type", "{{ .Type }}")
tab.AddColumn("Default", "{{ .Default }}")
tab.AddColumn("Description", "{{ .Description }}")
for _, flag := range flags {
tab.AddRow(flag)
}
return tab.Write(globalOptions.stdout)
},
}
func init() {
cmdRoot.AddCommand(featuresCmd)
}

View File

@ -0,0 +1,140 @@
package feature
import (
"fmt"
"sort"
"strconv"
"strings"
)
type state string
type FlagName string
const (
// Alpha features are disabled by default. They do not guarantee any backwards compatibility and may change in arbitrary ways between restic versions.
Alpha state = "alpha"
// Beta features are enabled by default. They may still change, but incompatible changes should be avoided.
Beta state = "beta"
// Stable features are always enabled
Stable state = "stable"
// Deprecated features are always disabled
Deprecated state = "deprecated"
)
type FlagDesc struct {
Type state
Description string
}
type FlagSet struct {
flags map[FlagName]*FlagDesc
enabled map[FlagName]bool
}
func New() *FlagSet {
return &FlagSet{}
}
func getDefault(phase state) bool {
switch phase {
case Alpha, Deprecated:
return false
case Beta, Stable:
return true
default:
panic("unknown feature phase")
}
}
func (f *FlagSet) SetFlags(flags map[FlagName]FlagDesc) {
f.flags = map[FlagName]*FlagDesc{}
f.enabled = map[FlagName]bool{}
for name, flag := range flags {
fcopy := flag
f.flags[name] = &fcopy
f.enabled[name] = getDefault(fcopy.Type)
}
}
func (f *FlagSet) Apply(flags string) error {
if flags == "" {
return nil
}
selection := make(map[string]bool)
for _, flag := range strings.Split(flags, ",") {
parts := strings.SplitN(flag, "=", 2)
name := parts[0]
value := "true"
if len(parts) == 2 {
value = parts[1]
}
isEnabled, err := strconv.ParseBool(value)
if err != nil {
return fmt.Errorf("failed to parse value %q for feature flag %v: %w", value, name, err)
}
selection[name] = isEnabled
}
for name, value := range selection {
fname := FlagName(name)
flag := f.flags[fname]
if flag == nil {
return fmt.Errorf("unknown feature flag %q", name)
}
switch flag.Type {
case Alpha, Beta:
f.enabled[fname] = value
case Stable:
// FIXME print warning
case Deprecated:
// FIXME print warning
default:
panic("unknown feature phase")
}
}
return nil
}
func (f *FlagSet) Enabled(name FlagName) bool {
isEnabled, ok := f.enabled[name]
if !ok {
panic(fmt.Sprintf("unknown feature flag %v", name))
}
return isEnabled
}
// Help contains information about a feature.
type Help struct {
Name string
Type string
Default bool
Description string
}
func (f *FlagSet) List() []Help {
var help []Help
for name, flag := range f.flags {
help = append(help, Help{
Name: string(name),
Type: string(flag.Type),
Default: getDefault(flag.Type),
Description: flag.Description,
})
}
sort.Slice(help, func(i, j int) bool {
return strings.Compare(help[i].Name, help[j].Name) < 0
})
return help
}

View File

@ -0,0 +1,15 @@
package feature
// Flag is named such that checking for a feature uses `feature.Flag.Enabled(feature.ExampleFeature)`.
var Flag = New()
// flag names are written in kebab-case
const (
ExampleFeature FlagName = "example-feature"
)
func init() {
Flag.SetFlags(map[FlagName]FlagDesc{
ExampleFeature: {Type: Alpha, Description: "just for testing"},
})
}

View File

@ -0,0 +1,29 @@
package feature
import (
"fmt"
"testing"
)
// TestSetFlag temporarily sets a feature flag to the given value until the
// returned function is called.
//
// Usage
// ```
// defer TestSetFlag(t, features.Flags, features.ExampleFlag, true)()
// ```
func TestSetFlag(t *testing.T, f *FlagSet, flag FlagName, value bool) func() {
current := f.Enabled(flag)
if err := f.Apply(fmt.Sprintf("%s=%v", flag, value)); err != nil {
// not reachable
panic(err)
}
return func() {
if err := f.Apply(fmt.Sprintf("%s=%v", flag, current)); err != nil {
// not reachable
panic(err)
}
}
}