Skip to content

Commit

Permalink
feat: add FillID method for Service, Route and Consumer (#299)
Browse files Browse the repository at this point in the history
  • Loading branch information
czeslavo authored Apr 24, 2023
1 parent be56889 commit 0c57bca
Show file tree
Hide file tree
Showing 3 changed files with 259 additions and 0 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@
- [0.2.0](#020)
- [0.1.0](#010)

## [v0.41.0]

> Release date: TBD
- Added `FillID` method for `Service`, `Route` and `Consumer` entities. It allows
setting a deterministic ID to an entity.
[#299](https://github.com/Kong/go-kong/pull/299)

## [v0.40.0]

> Release date: 2023/04/07
Expand Down
126 changes: 126 additions & 0 deletions kong/ids.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package kong

import (
"fmt"
"reflect"

"github.com/google/uuid"
)

// FillID fills the ID of an entity. It is a no-op if the entity already has an ID.
// ID is generated in a deterministic way using UUIDv5. The UUIDv5 namespace is different for each entity type.
// The name used to generate the ID for Service is Service.Name.
func (s *Service) FillID() error {
if s == nil {
return fmt.Errorf("service is nil")
}
if s.ID != nil {
// ID already set, do nothing.
return nil
}
if s.Name == nil || *s.Name == "" {
return fmt.Errorf("service name is required")
}

gen, err := idGeneratorFor(s)
if err != nil {
return fmt.Errorf("could not get id generator: %w", err)
}

s.ID = gen.buildIDFor(*s.Name)
return nil
}

// FillID fills the ID of an entity. It is a no-op if the entity already has an ID.
// ID is generated in a deterministic way using UUIDv5. The UUIDv5 namespace is different for each entity type.
// The name used to generate the ID for Route is Route.Name.
func (r *Route) FillID() error {
if r == nil {
return fmt.Errorf("route is nil")
}
if r.ID != nil {
// ID already set, do nothing.
return nil
}
if r.Name == nil || *r.Name == "" {
return fmt.Errorf("route name is required")
}

gen, err := idGeneratorFor(r)
if err != nil {
return fmt.Errorf("could not get id generator: %w", err)
}

r.ID = gen.buildIDFor(*r.Name)
return nil
}

// FillID fills the ID of an entity. It is a no-op if the entity already has an ID.
// ID is generated in a deterministic way using UUIDv5. The UUIDv5 namespace is different for each entity type.
// The name used to generate the ID for Consumer is Consumer.Username.
func (c *Consumer) FillID() error {
if c == nil {
return fmt.Errorf("consumer is nil")
}
if c.ID != nil {
// ID already set, do nothing.
return nil
}
if c.Username == nil || *c.Username == "" {
return fmt.Errorf("consumer username is required")
}

gen, err := idGeneratorFor(c)
if err != nil {
return fmt.Errorf("could not get id generator: %w", err)
}

c.ID = gen.buildIDFor(*c.Username)
return nil
}

var (
// _kongEntitiesNamespace is the UUIDv5 namespace used to generate IDs for Kong entities.
_kongEntitiesNamespace = uuid.MustParse("fd02801f-0957-4a15-a55a-c8d9606f30b5")

// _idGenerators is a map of entity type to ID generator.
// Plural names of entities are used as names for UUIDv5 namespaces to match Kong's behavior which uses schemas
// names for that purpose.
// See https://github.com/Kong/kong/blob/master/kong/db/schema/others/declarative_config.lua for reference.
_idGenerators = map[reflect.Type]idGenerator{
reflect.TypeOf(Service{}): newIDGeneratorFor("services"),
reflect.TypeOf(Route{}): newIDGeneratorFor("routes"),
reflect.TypeOf(Consumer{}): newIDGeneratorFor("consumers"),
}
)

type idGenerator struct {
namespace uuid.UUID
}

func (g idGenerator) buildIDFor(entityKey string) *string {
id := uuid.NewSHA1(g.namespace, []byte(entityKey)).String()
return &id
}

// newIDGeneratorFor returns a new ID generator for the given entity type. Should be used only to initialize _idGenerators.
func newIDGeneratorFor(entityPluralName string) idGenerator {
entityTypeNamespace := uuid.NewSHA1(_kongEntitiesNamespace, []byte(entityPluralName))
return idGenerator{namespace: entityTypeNamespace}
}

// IDFillable is a type constraint for entities that can be filled with an ID.
type IDFillable interface {
FillID() error
}

// idGeneratorFor returns the ID generator for the given entity type.
func idGeneratorFor(entity IDFillable) (idGenerator, error) {
generator, ok := _idGenerators[reflect.TypeOf(entity).Elem()]
if !ok {
// This should never happen, as the map is initialized with all supported entity types.
// If it does happen, it is a bug in the code.
return idGenerator{}, fmt.Errorf("unsupported entity type: '%T'", entity)
}
return generator, nil
}
125 changes: 125 additions & 0 deletions kong/ids_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package kong_test

import (
"testing"

"github.com/stretchr/testify/require"

"github.com/kong/go-kong/kong"
)

type fillEntityIDTestCase struct {
name string
entity kong.IDFillable

assertEntity func(t *testing.T, entity kong.IDFillable)
expectErr bool
}

func TestFillEntityID(t *testing.T) {
testCases := []fillEntityIDTestCase{
// Service
{
name: "service nil pointer",
entity: (*kong.Service)(nil),
expectErr: true,
},
{
name: "service with nil name",
entity: &kong.Service{},
expectErr: true,
},
{
name: "service with empty name",
entity: &kong.Service{Name: kong.String("")},
expectErr: true,
},
{
name: "service with name",
entity: &kong.Service{
Name: kong.String("some.service.name"),
},
assertEntity: func(t *testing.T, e kong.IDFillable) {
svc := e.(*kong.Service)
require.NotNil(t, svc.ID)
const expectedID = "d9bee1f8-db6e-5a37-9281-fd4aca16dc00"
require.Equal(t, expectedID, *svc.ID, "ID should be deterministic")
},
},
// Route
{
name: "route nil pointer",
entity: (*kong.Route)(nil),
expectErr: true,
},
{
name: "route with nil name",
entity: &kong.Route{},
expectErr: true,
},
{
name: "route with empty name",
entity: &kong.Route{Name: kong.String("")},
expectErr: true,
},
{
name: "route with name",
entity: &kong.Route{
Name: kong.String("some.service.name"),
},
assertEntity: func(t *testing.T, e kong.IDFillable) {
route := e.(*kong.Route)
require.NotNil(t, route.ID)

const expectedID = "7f0753cd-bff2-5f74-85b0-e8e2057d9500"
require.Equal(t, expectedID, *route.ID, "ID should be deterministic")
},
},
// Consumer
{
name: "consumer nil pointer",
entity: (*kong.Consumer)(nil),
expectErr: true,
},
{
name: "consumer with nil username",
entity: &kong.Consumer{},
expectErr: true,
},
{
name: "consumer with empty username",
entity: &kong.Consumer{Username: kong.String("")},
expectErr: true,
},
{
name: "consumer with username",
entity: &kong.Consumer{
Username: kong.String("some.username"),
},
assertEntity: func(t *testing.T, e kong.IDFillable) {
consumer := e.(*kong.Consumer)
require.NotNil(t, consumer.ID)

const expectedID = "782780c4-d345-5682-bdf6-722dccaf93e0"
require.Equal(t, expectedID, *consumer.ID, "ID should be deterministic")
},
},
}

for _, tc := range testCases {
tc := tc

t.Run(tc.name, func(t *testing.T) {
t.Parallel()

err := tc.entity.FillID()
if tc.expectErr {
require.Error(t, err)
return
}

require.NoError(t, err)
tc.assertEntity(t, tc.entity)
})
}
}

0 comments on commit 0c57bca

Please sign in to comment.