Skip to content

Commit

Permalink
registry: implement a generic registry for plugins
Browse files Browse the repository at this point in the history
This change adds a generic registry mechanism that is typesafe for users
and enforces consistent naming.

Signed-off-by: Hank Donnay <[email protected]>
  • Loading branch information
hdonnay committed Aug 2, 2023
1 parent 600e50d commit 6c73216
Show file tree
Hide file tree
Showing 3 changed files with 306 additions and 0 deletions.
31 changes: 31 additions & 0 deletions toolkit/registry/interfaces.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package registry

import (
"context"
"net"
"net/http"
)

// These are some common interfaces for plugins to optionally implement.
//
// Implementers can use blank assignments to check the interface is satisfied at
// compile time, and users can do a guarded type assertion to implement
// progressive enhancement.
type (
// CanHTTP is implemented by plugins that expect access to an http client.
//
// The passed Client can be stored by the callee, but the Client should be
// assumed to be shared. That is, it is unsafe to modify the Client and may
// panic the program.
CanHTTP interface {
HTTPClient(context.Context, *http.Client) error
}
// CanDial is implemented by plugins that expect general network access.
//
// The passed Dialer can be stored by the callee, but the Dialer should be
// assumed to be shared. That is, it is unsafe to modify the Dialer and may
// panic the program.
CanDial interface {
NetDialer(context.Context, *net.Dialer) error
}
)
213 changes: 213 additions & 0 deletions toolkit/registry/plugins.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
// Package registry is the central registry for all pluggable components in
// Claircore.
//
// Code referring to a pluggable component should use the name across API
// boundaries instead of passing instances of the objects.
package registry

import (
"context"
"fmt"
"path"
"reflect"
"sort"
"strings"
"sync"

"github.com/quay/claircore/toolkit/urn"
)

// Registry is the global registry.
var registry = struct {
sync.RWMutex
Lookup map[reflect.Type]interface{}
}{
Lookup: make(map[reflect.Type]interface{}),
}

// TypedReg is the per-type registry.
type typedReg[T any] struct {
sync.RWMutex
Lookup map[string]*Description[T]
AsPassed map[string]string
}

// GetNames returns the names for which the passed function reports true.
func (r *typedReg[T]) getNames(f func(*Description[T]) bool) []string {
r.RLock()
defer r.RUnlock()
ret := make([]string, 0, len(r.Lookup))
for n, d := range r.Lookup {
if f(d) {
ret = append(ret, r.AsPassed[n])
}
}
sort.Strings(ret)
return ret
}

// GetReg does the type assertion from the global registry to the per-type
// registry. The returned cleanup function should be called unconditionally. The
// returned *typedReg may be nil if the "create" argument is false.
func getReg[T any](create bool) (*typedReg[T], func()) {
var t T
key := reflect.TypeOf(t)
registry.RLock()
v, ok := registry.Lookup[key]
if !ok {
registry.RUnlock()
if !create {
return nil, func() {}
}
registry.Lock()
v2, ok := registry.Lookup[key]
if ok {
v = v2
} else {
v = &typedReg[T]{
Lookup: make(map[string]*Description[T]),
AsPassed: make(map[string]string),
}
registry.Lookup[key] = v
}
registry.Unlock()
registry.RLock()
}
reg := v.(*typedReg[T]) // Don't check the assertion, panic on purpose.
return reg, registry.RUnlock
}

// Default reports the names of the plugins that are default-enabled for the
// given type parameter.
func Default[T any]() []string {
tr, unlock := getReg[T](false)
defer unlock()
if tr == nil {
return nil
}
return tr.getNames(func(d *Description[T]) bool { return d.Default })
}

// All reports the names of the plugins that are registered for the given type
// parameter.
func All[T any]() []string {
tr, unlock := getReg[T](false)
defer unlock()
if tr == nil {
return nil
}
return tr.getNames(func(_ *Description[T]) bool { return true })
}

// Description is a description of all the information and hooks to construct a
// plugin of type T.
type Description[T any] struct {
// JSON Schema to validate a configuration against.
// See https://json-schema.org/ for information on the format.
ConfigSchema string
// New is a constructor for the given type.
//
// The passed function will unmarshal a configuration into the provided
// value. JSON is the default format, unless a Capability flag indicates
// otherwise.
New func(context.Context, func(any) error) (T, error)
// Capabilities flags.
//
// Meanings are set per-type.
Capabilities uint
// Default signals that the plugin should be enabled by default.
Default bool
}

// Register registers the provided description with the provided name in the
// type-specific registry indicated by the type parameter.
//
// Register may report errors if the name is already in use, or if the provided
// name is not valid.
func Register[T any](name string, desc Description[T]) error {
u, err := urn.Parse(name)
if err != nil {
return fmt.Errorf("registry: bad name: %w", err)
}
n, err := u.Name()
if err != nil {
return fmt.Errorf("registry: bad name: %w", err)
}
if err := checkname[T](&n); err != nil {
return fmt.Errorf("registry: bad name: %w", err)
}
key := u.Normalized()

tr, unlock := getReg[T](true)
defer unlock()
tr.Lock()
defer tr.Unlock()
if _, exists := tr.Lookup[key]; exists {
return fmt.Errorf("registry: name already registered: %q", name)
}
tr.Lookup[key] = &desc
tr.AsPassed[key] = name
return nil
}

// GetDescription returns Descriptions identified by the names in the registry
// indicated by the type parameter.
//
// An error will be reported if an unknown name is provided or if the type
// parameter has no names registered for it.
//
// The returned slice of Descriptions will have the same arity and order as the
// input names. That is, given `var names []string` and `var out []Description`,
// the i-th string is the i-th Description and vice-versa.
func GetDescription[T any](names ...string) ([]Description[T], error) {
keys := make([]string, len(names))
for i, n := range names {
u, err := urn.Parse(n)
if err != nil {
return nil, fmt.Errorf("registry: bad name at parameter %d: %w", i, err)
}
n, err := u.Name()
if err != nil {
return nil, fmt.Errorf("registry: bad name at parameter %d: %w", i, err)
}
if err := checkname[T](&n); err != nil {
return nil, fmt.Errorf("registry: bad name at parameter %d: %w", i, err)
}
keys[i] = u.Normalized()
}
tr, unlock := getReg[T](false)
defer unlock()
if tr == nil {
var t T
return nil, fmt.Errorf("registry: unknown type: %T", t)
}
tr.RLock()
defer tr.RUnlock()
ret := make([]Description[T], 0, len(names))
for _, k := range keys {
d, ok := tr.Lookup[k]
if !ok {
var t T
return nil, fmt.Errorf("registry: type %T: unknown name: %q", t, k)
}
ret = append(ret, *d)
}
return ret, nil
}

// Checkname makes sure the passed name is congruent with the expected type.
func checkname[T any](n *urn.Name) error {
var t *T
typ := reflect.TypeOf(t).Elem()
tk := strings.ToLower(typ.Name())
ts := strings.ToLower(path.Base(typ.PkgPath()))
switch {
case n.System != ts:
return fmt.Errorf("expected %q for system component, got %q", ts, n.System)
case n.Kind != tk:
return fmt.Errorf("expected %q for kind component, got %q", tk, n.Kind)
default:
// OK
}
return nil
}
62 changes: 62 additions & 0 deletions toolkit/registry/plugins_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package registry

import (
"context"
"fmt"
)

type MyPlugin interface {
Example()
}

func Example() {
// MyPlugin is an exported interface type.
desc := Description[MyPlugin]{
ConfigSchema: `{}`,
New: func(_ context.Context, _ func(_ any) error) (MyPlugin, error) {
return nil, nil
},
}
err := Register[MyPlugin](`urn:claircore:registry:myplugin:example`, desc)
if err != nil {
fmt.Println("error:", err)
} else {
fmt.Println("OK")
}
for _, n := range All[MyPlugin]() {
fmt.Println("all:", n)
}
for _, n := range Default[MyPlugin]() {
fmt.Println("default:", n)
}
// Output:
// OK
// all: urn:claircore:registry:myplugin:example
}

func Example_failure() {
// MyPlugin is an exported interface type.
desc := Description[MyPlugin]{
ConfigSchema: `{}`,
New: func(_ context.Context, _ func(_ any) error) (MyPlugin, error) {
return nil, nil
},
}
var err error
err = Register[MyPlugin](`urn:claircore:wrongpackage:myplugin:example`, desc)
if err != nil {
fmt.Println("error:", err)
} else {
fmt.Println("OK")
}
err = Register[MyPlugin](`urn:claircore:registry:wrongname:example`, desc)
if err != nil {
fmt.Println("error:", err)
} else {
fmt.Println("OK")
}

// Output:
// error: registry: bad name: expected "registry" for system component, got "wrongpackage"
// error: registry: bad name: expected "myplugin" for kind component, got "wrongname"
}

0 comments on commit 6c73216

Please sign in to comment.