-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
4205242
commit 50dfd33
Showing
7 changed files
with
361 additions
and
0 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,199 @@ | ||
package featuregatesattr | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"sync" | ||
|
||
"github.com/mandelsoft/goutils/general" | ||
"sigs.k8s.io/yaml" | ||
|
||
"ocm.software/ocm/api/datacontext" | ||
"ocm.software/ocm/api/utils/runtime" | ||
) | ||
|
||
const ( | ||
ATTR_KEY = "ocm.software/ocm/feature-gates" | ||
ATTR_SHORT = "featuregates" | ||
) | ||
|
||
func init() { | ||
datacontext.RegisterAttributeType(ATTR_KEY, AttributeType{}, ATTR_SHORT) | ||
} | ||
|
||
type AttributeType struct{} | ||
|
||
func (a AttributeType) Name() string { | ||
return ATTR_KEY | ||
} | ||
|
||
func (a AttributeType) Description() string { | ||
return ` | ||
*featuregates* Enable/Disable optional features of the OCM library. Optional, | ||
particular features modes and attributes can be configured, if supported by | ||
the feature implementation. | ||
` | ||
} | ||
|
||
func (a AttributeType) Encode(v interface{}, marshaller runtime.Marshaler) ([]byte, error) { | ||
switch t := v.(type) { | ||
case *Attribute: | ||
return json.Marshal(v) | ||
case string: | ||
_, err := a.Decode([]byte(t), runtime.DefaultYAMLEncoding) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return []byte(t), nil | ||
case []byte: | ||
_, err := a.Decode(t, runtime.DefaultYAMLEncoding) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return t, nil | ||
default: | ||
return nil, fmt.Errorf("feature gate config required") | ||
} | ||
} | ||
|
||
func (a AttributeType) Decode(data []byte, unmarshaller runtime.Unmarshaler) (interface{}, error) { | ||
var c Attribute | ||
err := yaml.Unmarshal(data, &c) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return &c, nil | ||
} | ||
|
||
//////////////////////////////////////////////////////////////////////////////// | ||
|
||
const FEATURE_DISABLED = "off" | ||
|
||
type Attribute struct { | ||
lock sync.Mutex | ||
|
||
Features map[string]*FeatureGate `json:"features"` | ||
} | ||
|
||
// FeatureGate store settings for a particular feature gate. | ||
// To be extended by additional config possibility. | ||
// Default behavior is to be enabled if entry is given | ||
// for a feature name and mode is not equal *off*. | ||
type FeatureGate struct { | ||
Mode string `json:"mode"` | ||
Attributes map[string]json.RawMessage `json:"attributes,omitempty"` | ||
} | ||
|
||
func New() *Attribute { | ||
return &Attribute{Features: map[string]*FeatureGate{}} | ||
} | ||
|
||
func (a *Attribute) EnableFeature(name string, state *FeatureGate) { | ||
a.lock.Lock() | ||
defer a.lock.Unlock() | ||
|
||
if state == nil { | ||
state = &FeatureGate{} | ||
} | ||
if state.Mode == FEATURE_DISABLED { | ||
state.Mode = "" | ||
} | ||
a.Features[name] = state | ||
} | ||
|
||
func (a *Attribute) SetFeature(name string, state *FeatureGate) { | ||
a.lock.Lock() | ||
defer a.lock.Unlock() | ||
|
||
if state == nil { | ||
state = &FeatureGate{} | ||
} | ||
a.Features[name] = state | ||
} | ||
|
||
func (a *Attribute) DisableFeature(name string) { | ||
a.lock.Lock() | ||
defer a.lock.Unlock() | ||
|
||
a.Features[name] = &FeatureGate{Mode: "off"} | ||
} | ||
|
||
func (a *Attribute) DefaultFeature(name string) { | ||
a.lock.Lock() | ||
defer a.lock.Unlock() | ||
|
||
delete(a.Features, name) | ||
} | ||
|
||
func (a *Attribute) IsEnabled(name string, def ...bool) bool { | ||
return a.GetFeature(name, def...).Mode != FEATURE_DISABLED | ||
} | ||
|
||
func (a *Attribute) GetFeature(name string, def ...bool) *FeatureGate { | ||
a.lock.Lock() | ||
defer a.lock.Unlock() | ||
|
||
g, ok := a.Features[name] | ||
if !ok { | ||
g = &FeatureGate{} | ||
if !general.Optional(def...) { | ||
g.Mode = FEATURE_DISABLED | ||
} | ||
} | ||
return g | ||
} | ||
|
||
//////////////////////////////////////////////////////////////////////////////// | ||
|
||
func Get(ctx datacontext.Context) *Attribute { | ||
v := ctx.GetAttributes().GetAttribute(ATTR_KEY) | ||
if v == nil { | ||
v = New() | ||
} | ||
return v.(*Attribute) | ||
} | ||
|
||
func Set(ctx datacontext.Context, c *Attribute) { | ||
ctx.GetAttributes().SetAttribute(ATTR_KEY, c) | ||
} | ||
|
||
var lock sync.Mutex | ||
|
||
func get(ctx datacontext.Context) *Attribute { | ||
attrs := ctx.GetAttributes() | ||
v := attrs.GetAttribute(ATTR_KEY) | ||
|
||
if v == nil { | ||
v = New() | ||
attrs.SetAttribute(ATTR_KEY, v) | ||
} | ||
return v.(*Attribute) | ||
} | ||
|
||
func SetFeature(ctx datacontext.Context, name string, state *FeatureGate) { | ||
lock.Lock() | ||
defer lock.Unlock() | ||
|
||
get(ctx).SetFeature(name, state) | ||
} | ||
|
||
func EnableFeature(ctx datacontext.Context, name string, state *FeatureGate) { | ||
lock.Lock() | ||
defer lock.Unlock() | ||
|
||
get(ctx).EnableFeature(name, state) | ||
} | ||
|
||
func DisableFeature(ctx datacontext.Context, name string) { | ||
lock.Lock() | ||
defer lock.Unlock() | ||
|
||
get(ctx).DisableFeature(name) | ||
} | ||
|
||
func DefaultFeature(ctx datacontext.Context, name string) { | ||
lock.Lock() | ||
defer lock.Unlock() | ||
|
||
get(ctx).DefaultFeature(name) | ||
} |
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,76 @@ | ||
package featuregates_test | ||
|
||
import ( | ||
. "github.com/onsi/ginkgo/v2" | ||
. "github.com/onsi/gomega" | ||
"ocm.software/ocm/api/datacontext/attrs/featuregatesattr" | ||
"ocm.software/ocm/api/datacontext/config/attrs" | ||
"ocm.software/ocm/api/datacontext/config/featuregates" | ||
|
||
"ocm.software/ocm/api/config" | ||
"ocm.software/ocm/api/datacontext" | ||
) | ||
|
||
var _ = Describe("feature gates", func() { | ||
var ctx config.Context | ||
|
||
BeforeEach(func() { | ||
ctx = config.WithSharedAttributes(datacontext.New(nil)).New() | ||
}) | ||
|
||
Context("applies", func() { | ||
It("handles default", func() { | ||
a := featuregatesattr.Get(ctx) | ||
|
||
Expect(a.IsEnabled("test")).To(BeFalse()) | ||
Expect(a.IsEnabled("test", true)).To(BeTrue()) | ||
g := a.GetFeature("test", true) | ||
Expect(g).NotTo(BeNil()) | ||
Expect(g.Mode).To(Equal("")) | ||
}) | ||
|
||
It("enables feature", func() { | ||
cfg := featuregates.New() | ||
cfg.EnableFeature("test", &featuregates.FeatureGate{Mode: "on"}) | ||
ctx.ApplyConfig(cfg, "manual") | ||
|
||
a := featuregatesattr.Get(ctx) | ||
|
||
Expect(a.IsEnabled("test")).To(BeTrue()) | ||
Expect(a.IsEnabled("test", true)).To(BeTrue()) | ||
g := a.GetFeature("test") | ||
Expect(g).NotTo(BeNil()) | ||
Expect(g.Mode).To(Equal("on")) | ||
}) | ||
|
||
It("disables feature", func() { | ||
cfg := featuregates.New() | ||
cfg.DisableFeature("test") | ||
ctx.ApplyConfig(cfg, "manual") | ||
|
||
a := featuregatesattr.Get(ctx) | ||
|
||
Expect(a.IsEnabled("test")).To(BeFalse()) | ||
Expect(a.IsEnabled("test", true)).To(BeFalse()) | ||
}) | ||
|
||
It("handle attribute config", func() { | ||
cfg := featuregatesattr.New() | ||
cfg.EnableFeature("test", &featuregates.FeatureGate{Mode: "on"}) | ||
|
||
spec := attrs.New() | ||
Expect(spec.AddAttribute(featuregatesattr.ATTR_KEY, cfg)).To(Succeed()) | ||
Expect(ctx.ApplyConfig(spec, "test")).To(Succeed()) | ||
|
||
ctx.ApplyConfig(spec, "manual") | ||
|
||
a := featuregatesattr.Get(ctx) | ||
|
||
Expect(a.IsEnabled("test")).To(BeTrue()) | ||
g := a.GetFeature("test") | ||
Expect(g).NotTo(BeNil()) | ||
Expect(g.Mode).To(Equal("on")) | ||
}) | ||
|
||
}) | ||
}) |
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,13 @@ | ||
package featuregates_test | ||
|
||
import ( | ||
"testing" | ||
|
||
. "github.com/onsi/ginkgo/v2" | ||
. "github.com/onsi/gomega" | ||
) | ||
|
||
func TestConfig(t *testing.T) { | ||
RegisterFailHandler(Fail) | ||
RunSpecs(t, "Feature Gates Config Test Suite") | ||
} |
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,69 @@ | ||
package featuregates | ||
|
||
import ( | ||
cfgcpi "ocm.software/ocm/api/config/cpi" | ||
"ocm.software/ocm/api/datacontext/attrs/featuregatesattr" | ||
"ocm.software/ocm/api/utils/runtime" | ||
) | ||
|
||
const ( | ||
ConfigType = "featuregates" + cfgcpi.OCM_CONFIG_TYPE_SUFFIX | ||
ConfigTypeV1 = ConfigType + runtime.VersionSeparator + "v1" | ||
) | ||
|
||
func init() { | ||
cfgcpi.RegisterConfigType(cfgcpi.NewConfigType[*Config](ConfigType, usage)) | ||
cfgcpi.RegisterConfigType(cfgcpi.NewConfigType[*Config](ConfigTypeV1, usage)) | ||
} | ||
|
||
type FeatureGate = featuregatesattr.FeatureGate | ||
|
||
// Config describes a memory based repository interface. | ||
type Config struct { | ||
runtime.ObjectVersionedType `json:",inline"` | ||
featuregatesattr.Attribute `json:",inline"` | ||
} | ||
|
||
// New creates a new memory ConfigSpec. | ||
func New() *Config { | ||
return &Config{ | ||
ObjectVersionedType: runtime.NewVersionedTypedObject(ConfigType), | ||
Attribute: *featuregatesattr.New(), | ||
} | ||
} | ||
|
||
func (a *Config) GetType() string { | ||
return ConfigType | ||
} | ||
|
||
func (a *Config) ApplyTo(ctx cfgcpi.Context, target interface{}) error { | ||
t, ok := target.(cfgcpi.Context) | ||
if !ok { | ||
return cfgcpi.ErrNoContext(ConfigType) | ||
} | ||
if len(a.Features) == 0 { | ||
return nil | ||
} | ||
for n, g := range a.Features { | ||
featuregatesattr.SetFeature(t, n, g) | ||
} | ||
return nil | ||
} | ||
|
||
const usage = ` | ||
The config type <code>` + ConfigType + `</code> can be used to define a list | ||
of feature gate settings: | ||
<pre> | ||
type: ` + ConfigType + ` | ||
features: | ||
<name>: { | ||
mode: off | <any key to enable> | ||
attributes: { | ||
<name>: <any yaml value> | ||
... | ||
} | ||
} | ||
... | ||
</pre> | ||
` |
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