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: "", + Description: "", + Enabled: false, + } + s = gates.GetFeature(n) + } + + o := &Object{ + FeatureGate: *def, + Settings: *s, + } + result = append(result, o) + } + return result, nil +} + +func (h *TypeHandler) Get(elemspec utils.ElemSpec) ([]output.Object, error) { + def := featuregates.DefaultRegistry().Get(elemspec.String()) + + if def == nil { + def = &featuregates.FeatureGate{ + Name: elemspec.String(), + Short: "", + Description: "", + Enabled: false, + } + } + s := featuregatesattr.Get(h.octx).GetFeature(elemspec.String(), false) + return []output.Object{ + &Object{ + FeatureGate: *def, + Settings: *s, + }, + }, nil +} diff --git a/cmds/ocm/commands/ocmcmds/featuregates/cmd.go b/cmds/ocm/commands/ocmcmds/featuregates/cmd.go new file mode 100644 index 0000000000..346c5db067 --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/featuregates/cmd.go @@ -0,0 +1,26 @@ +package featuregates + +import ( + "github.com/spf13/cobra" + + clictx "ocm.software/ocm/api/cli" + "ocm.software/ocm/cmds/ocm/commands/misccmds/action/execute" + "ocm.software/ocm/cmds/ocm/commands/ocmcmds/featuregates/get" + "ocm.software/ocm/cmds/ocm/commands/ocmcmds/names" + "ocm.software/ocm/cmds/ocm/common/utils" +) + +var Names = names.FeatureGates + +// NewCommand creates a new command. +func NewCommand(ctx clictx.Context) *cobra.Command { + cmd := utils.MassageCommand(&cobra.Command{ + Short: "Commands acting on feature gates", + }, Names...) + AddCommands(ctx, cmd) + return cmd +} + +func AddCommands(ctx clictx.Context, cmd *cobra.Command) { + cmd.AddCommand(execute.NewCommand(ctx, get.Verb)) +} diff --git a/cmds/ocm/commands/ocmcmds/featuregates/get/cmd.go b/cmds/ocm/commands/ocmcmds/featuregates/get/cmd.go new file mode 100644 index 0000000000..ea81de1656 --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/featuregates/get/cmd.go @@ -0,0 +1,119 @@ +package get + +import ( + "encoding/json" + + "github.com/mandelsoft/goutils/general" + "github.com/spf13/cobra" + + clictx "ocm.software/ocm/api/cli" + "ocm.software/ocm/api/credentials" + "ocm.software/ocm/api/datacontext/attrs/featuregatesattr" + "ocm.software/ocm/api/ocm/extensions/featuregates" + handler "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/handlers/featurehdlr" + "ocm.software/ocm/cmds/ocm/commands/ocmcmds/names" + "ocm.software/ocm/cmds/ocm/commands/verbs" + "ocm.software/ocm/cmds/ocm/common/output" + "ocm.software/ocm/cmds/ocm/common/processing" + "ocm.software/ocm/cmds/ocm/common/utils" +) + +var ( + Names = names.FeatureGates + Verb = verbs.Get +) + +type Command struct { + utils.BaseCommand + + Names []string + OutputMode string + MatcherType string + + Matcher credentials.IdentityMatcher + Consumer credentials.ConsumerIdentity +} + +// NewCommand creates a new ctf command. +func NewCommand(ctx clictx.Context, names ...string) *cobra.Command { + return utils.SetupCommand( + &Command{ + BaseCommand: utils.NewBaseCommand(ctx, output.OutputOptions(outputs)), + }, + utils.Names(Names, names...)..., + ) +} + +func (o *Command) ForName(name string) *cobra.Command { + return &cobra.Command{ + Use: "[] {}", + Short: "list feature gates", + Args: cobra.MinimumNArgs(0), + Long: ` +Show feature gates and the activation. + +The following feature gates are supported: +` + featuregates.Usage(featuregates.DefaultRegistry()), + Example: ` +$ ocm get featuregates +`, + Annotations: map[string]string{"ExampleCodeStyle": "bash"}, + } +} + +func (o *Command) Complete(args []string) error { + o.Names = args + return nil +} + +func (o *Command) Run() error { + hdlr := handler.NewTypeHandler(o.Context.OCMContext()) + return utils.HandleArgs(output.From(o), hdlr, o.Names...) +} + +//////////////////////////////////////////////////////////////////////////// + +func TableOutput(opts *output.Options, mapping processing.MappingFunction, wide ...string) *output.TableOutput { + def := &output.TableOutput{ + Headers: output.Fields("FEATURE", "ENABLED", "MODE", "SHORT", wide), + Options: opts, + Mapping: mapping, + } + return def +} + +///////////////////////////////////////////////////////////////////////////// + +var outputs = output.NewOutputs(getRegular, output.Outputs{ + "wide": getWide, +}).AddManifestOutputs() + +func getRegular(opts *output.Options) output.Output { + return TableOutput(opts, mapGetRegularOutput).New() +} + +func getWide(opts *output.Options) output.Output { + return TableOutput(opts, mapGetWideOutput, "DESCRIPTION", "ATTRIBUTES").New() +} + +func mapGetRegularOutput(e interface{}) interface{} { + p := handler.Elem(e) + + return []string{p.Name, general.Conditional(p.Mode == featuregatesattr.FEATURE_DISABLED, "disabled", "enabled"), p.Mode, p.Short} +} + +func mapGetWideOutput(e interface{}) interface{} { + p := handler.Elem(e) + + attr := "" + if len(p.Attributes) > 0 { + data, err := json.Marshal(p.Attributes) + if err == nil { + attr = string(data) + } else { + attr = err.Error() + } + } + reg := output.Fields(mapGetRegularOutput(e)) + return output.Fields(reg, p.Description, attr) +} diff --git a/cmds/ocm/commands/ocmcmds/featuregates/get/cmd_test.go b/cmds/ocm/commands/ocmcmds/featuregates/get/cmd_test.go new file mode 100644 index 0000000000..2f8e4ce1cf --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/featuregates/get/cmd_test.go @@ -0,0 +1,62 @@ +//go:build unix + +package get_test + +import ( + "bytes" + + . "github.com/mandelsoft/goutils/testutils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "ocm.software/ocm/cmds/ocm/testhelper" +) + +const PLUGINS = "/testdata" + +var _ = Describe("Test Environment", func() { + var env *TestEnv + + BeforeEach(func() { + env = NewTestEnv() + }) + + AfterEach(func() { + env.Cleanup() + }) + + It("get features", func() { + buf := bytes.NewBuffer(nil) + Expect(env.CatchOutput(buf).Execute("-X", `featuregates={ "features": { "test": {"mode": "on"}}}`, "get", "fg")).To(Succeed()) + Expect(buf.String()).To(StringEqualTrimmedWithContext( + ` +FEATURE ENABLED MODE SHORT +test enabled on +`)) + }) + + It("get wide feature", func() { + buf := bytes.NewBuffer(nil) + Expect(env.CatchOutput(buf).Execute("-X", `featuregates={ "features": { "test": {"mode": "on", "attributes": { "attr": "value"}}}}`, "get", "fg", "-o", "wide")).To(Succeed()) + Expect(buf.String()).To(StringEqualTrimmedWithContext( + ` +FEATURE ENABLED MODE SHORT DESCRIPTION ATTRIBUTES +test enabled on {"attr":"value"} +`)) + }) + + It("get yaml feature", func() { + buf := bytes.NewBuffer(nil) + Expect(env.CatchOutput(buf).Execute("-X", `featuregates={ "features": { "test": {"mode": "on", "attributes": { "attr": "value"}}}}`, "get", "fg", "-o", "yaml")).To(Succeed()) + Expect(buf.String()).To(StringEqualTrimmedWithContext( + ` +--- +attributes: + attr: value +description: "" +enabled: false +mode: "on" +name: test +short: +`)) + }) +}) diff --git a/cmds/ocm/commands/ocmcmds/featuregates/get/suite_test.go b/cmds/ocm/commands/ocmcmds/featuregates/get/suite_test.go new file mode 100644 index 0000000000..941485f933 --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/featuregates/get/suite_test.go @@ -0,0 +1,13 @@ +package get_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "OCM get features") +} diff --git a/cmds/ocm/commands/ocmcmds/names/names.go b/cmds/ocm/commands/ocmcmds/names/names.go index 652c1c46cf..fa647198eb 100644 --- a/cmds/ocm/commands/ocmcmds/names/names.go +++ b/cmds/ocm/commands/ocmcmds/names/names.go @@ -14,6 +14,7 @@ var ( Versions = []string{"versions", "vers", "v"} Plugins = []string{"plugins", "plugin", "p"} Action = []string{"action"} + FeatureGates = []string{"featuregates", "featuregate", "fg"} RoutingSlips = []string{"routingslips", "routingslip", "rs"} PubSub = []string{"pubsub", "ps"} Verified = []string{"verified"} diff --git a/cmds/ocm/commands/verbs/get/cmd.go b/cmds/ocm/commands/verbs/get/cmd.go index b0c00f458e..a3a3ba9281 100644 --- a/cmds/ocm/commands/verbs/get/cmd.go +++ b/cmds/ocm/commands/verbs/get/cmd.go @@ -8,6 +8,7 @@ import ( credentials "ocm.software/ocm/cmds/ocm/commands/misccmds/credentials/get" artifacts "ocm.software/ocm/cmds/ocm/commands/ocicmds/artifacts/get" components "ocm.software/ocm/cmds/ocm/commands/ocmcmds/components/get" + features "ocm.software/ocm/cmds/ocm/commands/ocmcmds/featuregates/get" plugins "ocm.software/ocm/cmds/ocm/commands/ocmcmds/plugins/get" pubsub "ocm.software/ocm/cmds/ocm/commands/ocmcmds/pubsub/get" references "ocm.software/ocm/cmds/ocm/commands/ocmcmds/references/get" @@ -35,5 +36,6 @@ func NewCommand(ctx clictx.Context) *cobra.Command { cmd.AddCommand(config.NewCommand(ctx)) cmd.AddCommand(pubsub.NewCommand(ctx)) cmd.AddCommand(verified.NewCommand(ctx)) + cmd.AddCommand(features.NewCommand(ctx)) return cmd } diff --git a/docs/reference/ocm.md b/docs/reference/ocm.md index 3dd9541eb2..d958bf23f7 100644 --- a/docs/reference/ocm.md +++ b/docs/reference/ocm.md @@ -306,6 +306,11 @@ The value can be a simple type or a JSON/YAML string for complex values 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_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 [] ... * [ocm get componentversions](ocm_get_componentversions.md) — get component version * [ocm get config](ocm_get_config.md) — Get evaluated config for actual command call * [ocm get credentials](ocm_get_credentials.md) — Get credentials for a dedicated consumer spec +* [ocm get featuregates](ocm_get_featuregates.md) — list feature gates * [ocm get plugins](ocm_get_plugins.md) — get plugins * [ocm get pubsub](ocm_get_pubsub.md) — Get the pubsub spec for an ocm repository * [ocm get references](ocm_get_references.md) — get references of a component version diff --git a/docs/reference/ocm_get_featuregates.md b/docs/reference/ocm_get_featuregates.md new file mode 100644 index 0000000000..1790b40e81 --- /dev/null +++ b/docs/reference/ocm_get_featuregates.md @@ -0,0 +1,50 @@ +## ocm get featuregates — List Feature Gates + +### Synopsis + +```bash +ocm get featuregates [] {} +``` + +#### Aliases + +```text +featuregates, featuregate, fg +``` + +### Options + +```text + -h, --help help for featuregates + -o, --output string output mode (JSON, json, wide, yaml) + -s, --sort stringArray sort fields +``` + +### Description + +Show feature gates and the activation. + +The following feature gates are supported: + + +With the option --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 [] ... * ocm ocm commontransportarchive — Commands acting on common transport archives * ocm ocm componentarchive — Commands acting on component archives * ocm ocm componentversions — Commands acting on components +* ocm ocm featuregates — Commands acting on feature gates * ocm ocm plugins — Commands related to OCM plugins * ocm ocm pubsub — Commands acting on sub/sub specifications * ocm ocm references — Commands related to component references in component versions