diff --git a/api/datacontext/attrs/featuregatesattr/attr.go b/api/datacontext/attrs/featuregatesattr/attr.go
new file mode 100644
index 0000000000..20e788d0d8
--- /dev/null
+++ b/api/datacontext/attrs/featuregatesattr/attr.go
@@ -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)
+}
diff --git a/api/datacontext/attrs/init.go b/api/datacontext/attrs/init.go
index 9b87f58cb3..0230b3de92 100644
--- a/api/datacontext/attrs/init.go
+++ b/api/datacontext/attrs/init.go
@@ -1,6 +1,7 @@
package attrs
import (
+ _ "ocm.software/ocm/api/datacontext/attrs/featuregatesattr"
_ "ocm.software/ocm/api/datacontext/attrs/logforward"
_ "ocm.software/ocm/api/datacontext/attrs/rootcertsattr"
_ "ocm.software/ocm/api/datacontext/attrs/tmpcache"
diff --git a/api/datacontext/config/featuregates/config_test.go b/api/datacontext/config/featuregates/config_test.go
new file mode 100644
index 0000000000..956ac4a17c
--- /dev/null
+++ b/api/datacontext/config/featuregates/config_test.go
@@ -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"))
+ })
+
+ })
+})
diff --git a/api/datacontext/config/featuregates/suite_test.go b/api/datacontext/config/featuregates/suite_test.go
new file mode 100644
index 0000000000..e2428a77c7
--- /dev/null
+++ b/api/datacontext/config/featuregates/suite_test.go
@@ -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")
+}
diff --git a/api/datacontext/config/featuregates/type.go b/api/datacontext/config/featuregates/type.go
new file mode 100644
index 0000000000..54d58c1d34
--- /dev/null
+++ b/api/datacontext/config/featuregates/type.go
@@ -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 ` + ConfigType + `
can be used to define a list
+of feature gate settings:
+
+
+ type: ` + ConfigType + ` + features: + <name>: { + mode: off | <any key to enable> + attributes: { + <name>: <any yaml value> + ... + } + } + ... ++` diff --git a/api/datacontext/config/init.go b/api/datacontext/config/init.go index 8655415cd0..cc0ec14664 100644 --- a/api/datacontext/config/init.go +++ b/api/datacontext/config/init.go @@ -2,5 +2,6 @@ package config import ( _ "ocm.software/ocm/api/datacontext/config/attrs" + _ "ocm.software/ocm/api/datacontext/config/featuregates" _ "ocm.software/ocm/api/datacontext/config/logging" ) diff --git a/api/datacontext/context.go b/api/datacontext/context.go index d883bec958..f13aa74954 100644 --- a/api/datacontext/context.go +++ b/api/datacontext/context.go @@ -383,6 +383,8 @@ func (c *_attributes) SetAttribute(name string, value interface{}) error { if *c.updater != nil { (*c.updater).Update() } + } else { + ocmlog.Logger().LogError(err, "cannot set context attribute", "attr", name, "value", value) } return err }