-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4666 from MichaelEischer/feature-flags
Implement feature flags
- Loading branch information
Showing
9 changed files
with
462 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
Enhancement: Add support for feature flags | ||
|
||
Restic now supports feature flags that can be used to enable and disable | ||
experimental features. The flags can be set using the environment variable | ||
`RESTIC_FEATURES`. To get a list of currently supported feature flags, | ||
run the `features` command. | ||
|
||
https://github.com/restic/restic/issues/4601 | ||
https://github.com/restic/restic/pull/4666 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
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" | ||
) | ||
|
||
var featuresCmd = &cobra.Command{ | ||
Use: "features", | ||
Short: "Print list of feature flags", | ||
Long: ` | ||
The "features" command prints a list of supported feature flags. | ||
To pass feature flags to restic, set the RESTIC_FEATURES environment variable | ||
to "featureA=true,featureB=false". Specifying an unknown feature flag is an error. | ||
A feature can either be in alpha, beta, stable or deprecated state. | ||
An _alpha_ feature is disabled by default and may change in arbitrary ways between restic versions or be removed. | ||
A _beta_ feature is enabled by default, but still can change in minor ways or be removed. | ||
A _stable_ feature is always enabled and cannot be disabled. The flag will be removed in a future restic version. | ||
A _deprecated_ feature is always disabled and cannot be enabled. The flag will be removed in a future restic version. | ||
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, logWarning func(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: | ||
logWarning(fmt.Sprintf("feature flag %q is always enabled and will be removed in a future release", fname)) | ||
case Deprecated: | ||
logWarning(fmt.Sprintf("feature flag %q is always disabled and will be removed in a future release", fname)) | ||
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 | ||
} |
Oops, something went wrong.