forked from TrueCloudLab/restic
features: add basic feature flag implementation
This commit is contained in:
parent
0589da60b3
commit
5974a79497
4 changed files with 235 additions and 0 deletions
51
cmd/restic/cmd_features.go
Normal file
51
cmd/restic/cmd_features.go
Normal 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)
|
||||
}
|
140
internal/feature/features.go
Normal file
140
internal/feature/features.go
Normal 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
|
||||
}
|
15
internal/feature/registry.go
Normal file
15
internal/feature/registry.go
Normal 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"},
|
||||
})
|
||||
}
|
29
internal/feature/testing.go
Normal file
29
internal/feature/testing.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue