diff --git a/internal/resource/registry.go b/internal/resource/registry.go new file mode 100644 index 0000000000..1afd67c678 --- /dev/null +++ b/internal/resource/registry.go @@ -0,0 +1,71 @@ +package resource + +import ( + "fmt" + "sync" + + "github.com/hashicorp/consul/proto-public/pbresource" +) + +type Registry interface { + // Register the given resource type and its hooks. + Register(reg Registration) + + // Resolve the given resource type and its hooks. + Resolve(typ *pbresource.Type) (reg Registration, ok bool) +} + +type Registration struct { + // Type is the GVK of the resource type. + Type *pbresource.Type + + // In the future, we'll add hooks, the controller etc. here. + // TODO: https://github.com/hashicorp/consul/pull/16622#discussion_r1134515909 +} + +// Hashable key for a resource type +type TypeKey string + +// Resource type registry +type TypeRegistry struct { + // registrations keyed by GVK + registrations map[string]Registration + lock sync.RWMutex +} + +func NewRegistry() Registry { + return &TypeRegistry{ + registrations: make(map[string]Registration), + } +} + +func (r *TypeRegistry) Register(registration Registration) { + r.lock.Lock() + defer r.lock.Unlock() + + typ := registration.Type + if typ.Group == "" || typ.GroupVersion == "" || typ.Kind == "" { + panic("type field(s) cannot be empty") + } + + key := ToGVK(registration.Type) + if _, ok := r.registrations[key]; ok { + panic(fmt.Sprintf("resource type %s already registered", key)) + } + + r.registrations[key] = registration +} + +func (r *TypeRegistry) Resolve(typ *pbresource.Type) (reg Registration, ok bool) { + r.lock.RLock() + defer r.lock.RUnlock() + + if registration, ok := r.registrations[ToGVK(typ)]; ok { + return registration, true + } + return Registration{}, false +} + +func ToGVK(resourceType *pbresource.Type) string { + return fmt.Sprintf("%s/%s/%s", resourceType.Group, resourceType.GroupVersion, resourceType.Kind) +} diff --git a/internal/resource/registry_test.go b/internal/resource/registry_test.go new file mode 100644 index 0000000000..5e8b326e33 --- /dev/null +++ b/internal/resource/registry_test.go @@ -0,0 +1,89 @@ +package resource + +import ( + "testing" + + "github.com/hashicorp/consul/proto-public/pbresource" + "github.com/stretchr/testify/assert" +) + +func TestRegister(t *testing.T) { + r := NewRegistry() + + serviceType := &pbresource.Type{ + Group: "mesh", + GroupVersion: "v1", + Kind: "service", + } + + // register + serviceRegistration := Registration{Type: serviceType} + r.Register(serviceRegistration) + + // register existing should panic + assertRegisterPanics(t, r.Register, serviceRegistration, "resource type mesh/v1/service already registered") + + // register empty Group should panic + assertRegisterPanics(t, r.Register, Registration{ + Type: &pbresource.Type{ + Group: "", + GroupVersion: "v1", + Kind: "service", + }, + }, "type field(s) cannot be empty") + + // register empty GroupVersion should panic + assertRegisterPanics(t, r.Register, Registration{ + Type: &pbresource.Type{ + Group: "mesh", + GroupVersion: "", + Kind: "service", + }, + }, "type field(s) cannot be empty") + + // register empty Kind should panic + assertRegisterPanics(t, r.Register, Registration{ + Type: &pbresource.Type{ + Group: "mesh", + GroupVersion: "v1", + Kind: "", + }, + }, "type field(s) cannot be empty") +} + +func assertRegisterPanics(t *testing.T, registerFn func(reg Registration), registration Registration, panicString string) { + defer func() { + if r := recover(); r == nil { + t.Errorf("expected panic, but none occurred") + } else { + errstr, ok := r.(string) + if !ok { + t.Errorf("unexpected error type returned from panic") + } else if errstr != panicString { + t.Errorf("expected %s error message but got: %s", panicString, errstr) + } + } + }() + + registerFn(registration) +} + +func TestResolve(t *testing.T) { + r := NewRegistry() + + serviceType := &pbresource.Type{ + Group: "mesh", + GroupVersion: "v1", + Kind: "service", + } + + // not found + _, ok := r.Resolve(serviceType) + assert.False(t, ok) + + // found + r.Register(Registration{Type: serviceType}) + registration, ok := r.Resolve(serviceType) + assert.True(t, ok) + assert.Equal(t, registration.Type, serviceType) +}