-
Notifications
You must be signed in to change notification settings - Fork 86
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
registry: implement a generic registry for plugins
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
Showing
3 changed files
with
306 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |