diff --git a/.github/config/wordlist.txt b/.github/config/wordlist.txt
index d38b0d4469..5711061ccf 100644
--- a/.github/config/wordlist.txt
+++ b/.github/config/wordlist.txt
@@ -91,6 +91,7 @@ executables
executoroptions
executorspecification
extensibility
+featuregates
filepath
filesystem
filesytem
diff --git a/api/config/internal/setup_test.go b/api/config/internal/setup_test.go
index 731f230cb3..84fbc575c9 100644
--- a/api/config/internal/setup_test.go
+++ b/api/config/internal/setup_test.go
@@ -10,7 +10,7 @@ import (
var _ = Describe("setup", func() {
It("creates initial", func() {
- Expect(len(config.DefaultContext().ConfigTypes().KnownTypeNames())).To(Equal(6))
- Expect(len(internal.DefaultConfigTypeScheme.KnownTypeNames())).To(Equal(6))
+ Expect(len(config.DefaultContext().ConfigTypes().KnownTypeNames())).To(Equal(8))
+ Expect(len(internal.DefaultConfigTypeScheme.KnownTypeNames())).To(Equal(8))
})
})
diff --git a/api/datacontext/attrs/featuregatesattr/attr.go b/api/datacontext/attrs/featuregatesattr/attr.go
new file mode 100644
index 0000000000..39c8f3b245
--- /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_SHORT = "featuregates"
+ ATTR_KEY = "ocm.software/ocm/" + ATTR_SHORT
+)
+
+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.
+Optionally, 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..3bbc6748df
--- /dev/null
+++ b/api/datacontext/config/featuregates/config_test.go
@@ -0,0 +1,75 @@
+package featuregates_test
+
+import (
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ "ocm.software/ocm/api/config"
+ "ocm.software/ocm/api/datacontext"
+ "ocm.software/ocm/api/datacontext/attrs/featuregatesattr"
+ "ocm.software/ocm/api/datacontext/config/attrs"
+ "ocm.software/ocm/api/datacontext/config/featuregates"
+)
+
+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", false)).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..8ce42f3d42
--- /dev/null
+++ b/api/datacontext/config/featuregates/type.go
@@ -0,0 +1,66 @@
+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 = featuregatesattr.ATTR_SHORT + 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)
+ }
+ 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 } diff --git a/api/ocm/extensions/featuregates/registry.go b/api/ocm/extensions/featuregates/registry.go new file mode 100644 index 0000000000..6e70fcbff6 --- /dev/null +++ b/api/ocm/extensions/featuregates/registry.go @@ -0,0 +1,87 @@ +package featuregates + +import ( + "sync" + + "github.com/mandelsoft/goutils/general" + "github.com/mandelsoft/goutils/maputils" + + "ocm.software/ocm/api/datacontext" + "ocm.software/ocm/api/datacontext/attrs/featuregatesattr" + "ocm.software/ocm/api/utils" + common "ocm.software/ocm/api/utils/misc" +) + +type FeatureGate struct { + Name string `json:"name"` + Short string `json:"short"` + Description string `json:"description"` + Enabled bool `json:"enabled"` +} + +func (f *FeatureGate) GetSettings(ctx datacontext.Context) *featuregatesattr.FeatureGate { + return featuregatesattr.Get(ctx).GetFeature(f.Name, f.Enabled) +} + +func (f *FeatureGate) IsEnabled(ctx datacontext.Context) bool { + return featuregatesattr.Get(ctx).IsEnabled(f.Name, f.Enabled) +} + +type Registry interface { + Register(gate *FeatureGate) + GetNames() []string + Get(name string) *FeatureGate +} + +type registry struct { + lock sync.Mutex + gates map[string]*FeatureGate +} + +var _ Registry = (*registry)(nil) + +func (r *registry) Register(g *FeatureGate) { + r.lock.Lock() + defer r.lock.Unlock() + + r.gates[g.Name] = g +} + +func (r *registry) GetNames() []string { + r.lock.Lock() + defer r.lock.Unlock() + + return maputils.OrderedKeys(r.gates) +} + +func (r *registry) Get(name string) *FeatureGate { + r.lock.Lock() + defer r.lock.Unlock() + + return r.gates[name] +} + +var defaultRegistry = ®istry{ + gates: map[string]*FeatureGate{}, +} + +func DefaultRegistry() Registry { + return defaultRegistry +} + +func Register(fg *FeatureGate) { + defaultRegistry.Register(fg) +} + +func Usage(reg Registry) string { + p, buf := common.NewBufferedPrinter() + for _, n := range reg.GetNames() { + a := reg.Get(n) + p.Printf("- Name: %s\n", n) + p.Printf(" Default: %s\n", general.Conditional(a.Enabled, "enabled", "disabled")) + if a.Description != "" { + p.Printf("%s\n", utils.IndentLines(a.Description, " ")) + } + } + return buf.String() +} diff --git a/cmds/ocm/commands/ocmcmds/cmd.go b/cmds/ocm/commands/ocmcmds/cmd.go index a35b28540b..efe10b89b8 100644 --- a/cmds/ocm/commands/ocmcmds/cmd.go +++ b/cmds/ocm/commands/ocmcmds/cmd.go @@ -7,6 +7,7 @@ import ( "ocm.software/ocm/cmds/ocm/commands/ocmcmds/componentarchive" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/components" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/ctf" + "ocm.software/ocm/cmds/ocm/commands/ocmcmds/featuregates" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/plugins" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/pubsub" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/references" @@ -42,6 +43,7 @@ func NewCommand(ctx clictx.Context) *cobra.Command { cmd.AddCommand(routingslips.NewCommand(ctx)) cmd.AddCommand(pubsub.NewCommand(ctx)) cmd.AddCommand(verified.NewCommand(ctx)) + cmd.AddCommand(featuregates.NewCommand(ctx)) cmd.AddCommand(utils.DocuCommandPath(topicocmrefs.New(ctx), "ocm")) cmd.AddCommand(utils.DocuCommandPath(topicocmaccessmethods.New(ctx), "ocm")) diff --git a/cmds/ocm/commands/ocmcmds/common/handlers/featurehdlr/sort.go b/cmds/ocm/commands/ocmcmds/common/handlers/featurehdlr/sort.go new file mode 100644 index 0000000000..2b358bb86f --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/common/handlers/featurehdlr/sort.go @@ -0,0 +1,17 @@ +package featurehdlr + +import ( + "strings" + + "ocm.software/ocm/cmds/ocm/common/processing" +) + +func Compare(a, b interface{}) int { + aa := a.(*Object) + ab := b.(*Object) + + return strings.Compare(aa.Name, ab.Name) +} + +// Sort is a processing chain sorting original objects provided by type handler. +var Sort = processing.Sort(Compare) diff --git a/cmds/ocm/commands/ocmcmds/common/handlers/featurehdlr/suite_test.go b/cmds/ocm/commands/ocmcmds/common/handlers/featurehdlr/suite_test.go new file mode 100644 index 0000000000..a33a843214 --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/common/handlers/featurehdlr/suite_test.go @@ -0,0 +1,13 @@ +package featurehdlr_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "feature handler Test Suite") +} diff --git a/cmds/ocm/commands/ocmcmds/common/handlers/featurehdlr/typehandler.go b/cmds/ocm/commands/ocmcmds/common/handlers/featurehdlr/typehandler.go new file mode 100644 index 0000000000..7bd0f0f2e4 --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/common/handlers/featurehdlr/typehandler.go @@ -0,0 +1,104 @@ +package featurehdlr + +import ( + "sort" + + "github.com/mandelsoft/goutils/maputils" + "github.com/mandelsoft/goutils/sliceutils" + + "ocm.software/ocm/api/datacontext/attrs/featuregatesattr" + "ocm.software/ocm/api/ocm" + "ocm.software/ocm/api/ocm/extensions/featuregates" + "ocm.software/ocm/cmds/ocm/common/output" + "ocm.software/ocm/cmds/ocm/common/utils" +) + +func Elem(e interface{}) *Object { + return e.(*Object) +} + +//////////////////////////////////////////////////////////////////////////////// + +type Settings = featuregatesattr.FeatureGate + +type Object struct { + featuregates.FeatureGate `json:",inline"` + Settings `json:",inline"` +} + +func CompareObject(a, b output.Object) int { + return Compare(a, b) +} + +func (o *Object) AsManifest() interface{} { + return o +} + +//////////////////////////////////////////////////////////////////////////////// + +type TypeHandler struct { + octx ocm.Context +} + +func NewTypeHandler(octx ocm.Context) utils.TypeHandler { + h := &TypeHandler{ + octx: octx, + } + return h +} + +func (h *TypeHandler) Close() error { + return nil +} + +func (h *TypeHandler) All() ([]output.Object, error) { + result := []output.Object{} + + gates := featuregatesattr.Get(h.octx) + list := sliceutils.AppendUnique(featuregates.DefaultRegistry().GetNames(), maputils.Keys(gates.Features)...) + sort.Strings(list) + + for _, n := range list { + var s *featuregatesattr.FeatureGate + + def := featuregates.DefaultRegistry().Get(n) + if def != nil { + s = def.GetSettings(h.octx) + } else { + def = &featuregates.FeatureGate{ + Name: n, + Short: "
ocm.software/ocm/featuregates
[featuregates
]: *featuregates* Enable/Disable optional features of the OCM library.
+
+ Optionally, particular features modes and attributes can be configured, if
+ supported by the feature implementation.
+
- ocm.software/signing/sigstore
[sigstore
]: *sigstore config* Configuration to use for sigstore based signing.
The following fields are used.
diff --git a/docs/reference/ocm_attributes.md b/docs/reference/ocm_attributes.md
index af981a5bf0..312407938a 100644
--- a/docs/reference/ocm_attributes.md
+++ b/docs/reference/ocm_attributes.md
@@ -198,6 +198,11 @@ OCM library:
the backend and descriptor updated will be persisted on AddVersion
or closing a provided existing component version.
+- ocm.software/ocm/featuregates
[featuregates
]: *featuregates* Enable/Disable optional features of the OCM library.
+
+ Optionally, particular features modes and attributes can be configured, if
+ supported by the feature implementation.
+
- ocm.software/signing/sigstore
[sigstore
]: *sigstore config* Configuration to use for sigstore based signing.
The following fields are used.
diff --git a/docs/reference/ocm_configfile.md b/docs/reference/ocm_configfile.md
index 4f5282064d..86472d5fa1 100644
--- a/docs/reference/ocm_configfile.md
+++ b/docs/reference/ocm_configfile.md
@@ -98,6 +98,22 @@ The following configuration types are supported:
config: ...
...
+- featuregates.config.ocm.software
+ The config type featuregates.config.ocm.software
can be used to define a list
+ of feature gate settings:
+
+ + type: featuregates.config.ocm.software + features: + <name>: { + mode: off | <any key to enable> + attributes: { + <name>: <any yaml value> + ... + } + } + ... +-
generic.config.ocm.software
The config type generic.config.ocm.software
can be used to define a list
of arbitrary configuration specifications and named configuration sets:
diff --git a/docs/reference/ocm_get.md b/docs/reference/ocm_get.md
index 28a479765e..67d1f3b433 100644
--- a/docs/reference/ocm_get.md
+++ b/docs/reference/ocm_get.md
@@ -25,6 +25,7 @@ ocm get [--output
the output mode can be selected.
+The following modes are supported:
+ -
(default)
+ - JSON
+ - json
+ - wide
+ - yaml
+
+### Examples
+
+```bash
+$ ocm get featuregates
+```
+
+### SEE ALSO
+
+#### Parents
+
+* [ocm get](ocm_get.md) — Get information about artifacts and components
+* [ocm](ocm.md) — Open Component Model command line client
+
diff --git a/docs/reference/ocm_ocm.md b/docs/reference/ocm_ocm.md
index 2c1f092df8..2f9907512e 100644
--- a/docs/reference/ocm_ocm.md
+++ b/docs/reference/ocm_ocm.md
@@ -24,6 +24,7 @@ ocm ocm [