Skip to content

Commit

Permalink
CLI command for feature gates
Browse files Browse the repository at this point in the history
  • Loading branch information
mandelsoft committed Nov 27, 2024
1 parent c6bcbdf commit b62059c
Show file tree
Hide file tree
Showing 15 changed files with 575 additions and 0 deletions.
87 changes: 87 additions & 0 deletions api/ocm/extensions/featuregates/registry.go
Original file line number Diff line number Diff line change
@@ -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 = &registry{
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()
}
3 changes: 3 additions & 0 deletions cmds/ocm/commands/ocmcmds/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -42,7 +43,9 @@ 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(topicocmrefs.New(ctx), "ocm"))
cmd.AddCommand(utils.DocuCommandPath(topicocmaccessmethods.New(ctx), "ocm"))
cmd.AddCommand(utils.DocuCommandPath(topicocmuploaders.New(ctx), "ocm"))
Expand Down
17 changes: 17 additions & 0 deletions cmds/ocm/commands/ocmcmds/common/handlers/featurehdlr/sort.go
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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, "Version handler Test Suite")
}
105 changes: 105 additions & 0 deletions cmds/ocm/commands/ocmcmds/common/handlers/featurehdlr/typehandler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
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: "<unknown>",
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())

enabled := false
if def == nil {
def = &featuregates.FeatureGate{
Name: elemspec.String(),
Short: "<unknown>",
Description: "",
Enabled: false,
}
}
s := featuregatesattr.Get(h.octx).GetFeature(elemspec.String(), enabled)
return []output.Object{
&Object{
FeatureGate: *def,
Settings: *s,
},
}, nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package featurehdlr_test

import (
"bytes"

. "github.com/mandelsoft/goutils/testutils"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
. "ocm.software/ocm/cmds/ocm/testhelper"

"github.com/mandelsoft/vfs/pkg/vfs"

"ocm.software/ocm/api/ocm/extensions/repositories/ctf"
"ocm.software/ocm/api/utils/accessio"
"ocm.software/ocm/api/utils/accessobj"
)

const (
ARCH = "ctf"
COMP = "acme.org/comp1"
VERS1 = "1.0.0"
VERS2 = "2.0.0"
)

var _ = Describe("version handler", func() {
var env *TestEnv

BeforeEach(func() {
env = NewTestEnv()

env.OCMCommonTransport(ARCH, accessio.FormatDirectory, func() {
env.Component(COMP, func() {
env.Version(VERS1)
env.Version(VERS2)
})
})

env.OCMContext().AddResolverRule(COMP, Must(ctf.NewRepositorySpec(accessobj.ACC_READONLY, ARCH, env)))
})

AfterEach(func() {
vfs.Cleanup(env)
})

Context("using resolvers", func() {
It("resolves versions", func() {
var buf bytes.Buffer
MustBeSuccessful(env.CatchOutput(&buf).Execute("list", "cv", COMP))
Expect(buf.String()).To(StringEqualTrimmedWithContext(`
COMPONENT VERSION MESSAGE
acme.org/comp1 1.0.0
acme.org/comp1 2.0.0
`))
})

It("provides error for non-matching resolver", func() {
var buf bytes.Buffer
MustBeSuccessful(env.CatchOutput(&buf).Execute("list", "cv", "acme.org/dummy"))
Expect(buf.String()).To(StringEqualTrimmedWithContext(`
COMPONENT VERSION MESSAGE
acme.org/dummy <unknown component version>
`))
})

It("provides error for non-matching component", func() {
var buf bytes.Buffer
MustBeSuccessful(env.CatchOutput(&buf).Execute("list", "cv", COMP+"/"+"dummy"))
Expect(buf.String()).To(StringEqualTrimmedWithContext(`
COMPONENT VERSION MESSAGE
acme.org/comp1/dummy <unknown component version>
`))
})
})
})
26 changes: 26 additions & 0 deletions cmds/ocm/commands/ocmcmds/featuregates/cmd.go
Original file line number Diff line number Diff line change
@@ -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 actions",
}, Names...)
AddCommands(ctx, cmd)
return cmd
}

func AddCommands(ctx clictx.Context, cmd *cobra.Command) {
cmd.AddCommand(execute.NewCommand(ctx, get.Verb))
}
Loading

0 comments on commit b62059c

Please sign in to comment.