Skip to content

Commit

Permalink
feat: Implement domain scoping (#261)
Browse files Browse the repository at this point in the history
* added support for domains

Signed-off-by: gunishmatta <[email protected]>

* added support for domains

Signed-off-by: gunishmatta <[email protected]>

* chore(deps): update actions/setup-go action to v5 (#237)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: gunishmatta <[email protected]>

* chore(deps): update cyclonedx/gh-gomod-generate-sbom action to v2 (#179)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: gunishmatta <[email protected]>

* Update openfeature/client.go

Co-authored-by: Michael Beemer <[email protected]>
Signed-off-by: Gunish Matta <[email protected]>
Signed-off-by: gunishmatta <[email protected]>

* Update openfeature/client.go

Co-authored-by: Michael Beemer <[email protected]>
Signed-off-by: Gunish Matta <[email protected]>
Signed-off-by: gunishmatta <[email protected]>

* code review changes

Signed-off-by: gunishmatta <[email protected]>

* code review changes

Signed-off-by: gunishmatta <[email protected]>

* code review changes

Signed-off-by: gunishmatta <[email protected]>

* code review changes

Signed-off-by: gunishmatta <[email protected]>

* feat: Added domain scoping #261
 Please enter the commit message for your chanes. Lines starting
 Author: Gunish Matta <[email protected]>

Signed-off-by: gunishmatta <[email protected]>

* chore(main): release 1.11.0 (#254)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Signed-off-by: gunishmatta <[email protected]>

* chore: bump Go to version 1.20 (#255)

* chore: bump Go to version 1.21

Signed-off-by: odubajDT <[email protected]>

* update readme with go version and note

Signed-off-by: Kavindu Dodanduwa <[email protected]>

* Update README.md

Co-authored-by: Michael Beemer <[email protected]>
Signed-off-by: Kavindu Dodanduwa <[email protected]>

---------

Signed-off-by: odubajDT <[email protected]>
Signed-off-by: Kavindu Dodanduwa <[email protected]>
Signed-off-by: Kavindu Dodanduwa <[email protected]>
Co-authored-by: Kavindu Dodanduwa <[email protected]>
Co-authored-by: Kavindu Dodanduwa <[email protected]>
Co-authored-by: Michael Beemer <[email protected]>
Signed-off-by: gunishmatta <[email protected]>

* code review changes

Signed-off-by: gunishmatta <[email protected]>

* code review changes

Signed-off-by: gunishmatta <[email protected]>

* code review changes

Signed-off-by: gunishmatta <[email protected]>

* code review changes

Signed-off-by: gunishmatta <[email protected]>

* fix(deps): update module github.com/cucumber/godog to v0.14.1 (#267)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: gunishmatta <[email protected]>

* chore(deps): update goreleaser/goreleaser-action action to v5 (#219)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: gunishmatta <[email protected]>

* chore(deps): update codecov/codecov-action action to v4 (#250)

* chore(deps): update codecov/codecov-action action to v4

* add codecov upload token

Signed-off-by: Michael Beemer <[email protected]>

---------

Signed-off-by: Michael Beemer <[email protected]>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Beemer <[email protected]>
Signed-off-by: gunishmatta <[email protected]>

* fix(deps): update module golang.org/x/exp to v0.0.0-20240416160154-fe59bbe5cc7f (#269)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: gunishmatta <[email protected]>

* chore(deps): update codecov/codecov-action action to v4.3.1 (#270)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: gunishmatta <[email protected]>

* Revert "code review changes"

This reverts commit 3472ee8.

Signed-off-by: gunishmatta <[email protected]>

* code review changes

Signed-off-by: gunishmatta <[email protected]>

---------

Signed-off-by: gunishmatta <[email protected]>
Signed-off-by: Gunish Matta <[email protected]>
Signed-off-by: odubajDT <[email protected]>
Signed-off-by: Kavindu Dodanduwa <[email protected]>
Signed-off-by: Kavindu Dodanduwa <[email protected]>
Signed-off-by: Michael Beemer <[email protected]>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Beemer <[email protected]>
Co-authored-by: Dave Henderson <[email protected]>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: odubajDT <[email protected]>
Co-authored-by: Kavindu Dodanduwa <[email protected]>
Co-authored-by: Kavindu Dodanduwa <[email protected]>
  • Loading branch information
8 people authored May 16, 2024
1 parent 266cfc0 commit a9e19dd
Show file tree
Hide file tree
Showing 9 changed files with 68 additions and 64 deletions.
28 changes: 13 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,16 +89,16 @@ See [here](https://pkg.go.dev/github.com/open-feature/go-sdk/pkg/openfeature) fo

## 🌟 Features

| Status | Features | Description |
| ------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|| [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. |
| Status | Features | Description |
| ------ |---------------------------------| --------------------------------------------------------------------------------------------------------------------------------- |
|| [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. |
|| [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). |
|| [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. |
|| [Logging](#logging) | Integrate with popular logging packages. |
|| [Named clients](#named-clients) | Utilize multiple providers in a single application. |
|| [Eventing](#eventing) | React to state changes in the provider or flag management system. |
|| [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. |
|| [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |
|| [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. |
|| [Logging](#logging) | Integrate with popular logging packages. |
|| [Domains](#domains) | Logically bind clients with providers.|
|| [Eventing](#eventing) | React to state changes in the provider or flag management system. |
|| [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. |
|| [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |

<sub>Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌</sub>

Expand All @@ -115,7 +115,7 @@ openfeature.SetProvider(MyProvider{})
```

In some situations, it may be beneficial to register multiple providers in the same application.
This is possible using [named clients](#named-clients), which is covered in more details below.
This is possible using [domains](#domains), which is covered in more details below.

### Targeting

Expand Down Expand Up @@ -190,11 +190,8 @@ c := openfeature.NewClient("log").WithLogger(l) // set the logger at client leve
[logr](https://github.com/go-logr/logr) uses incremental verbosity levels (akin to named levels but in integer form).
The SDK logs `info` at level `0` and `debug` at level `1`. Errors are always logged.

### Named clients

Clients can be given a name.
A name is a logical identifier which can be used to associate clients with a particular provider.
If a name has no associated provider, the global provider is used.
### Domains
Clients can be assigned to a domain. A domain is a logical identifier which can be used to associate clients with a particular provider. If a domain has no associated provider, the default provider is used.

```go
import "github.com/open-feature/go-sdk/openfeature"
Expand All @@ -210,6 +207,7 @@ clientWithDefault := openfeature.NewClient("")
clientForCache := openfeature.NewClient("clientForCache")
```


### Eventing

Events allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, provider readiness, or error conditions.
Expand Down
16 changes: 8 additions & 8 deletions openfeature/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ func (api *evaluationAPI) getProvider() FeatureProvider {
return api.defaultProvider
}

// setProvider sets a provider with client name. Returns an error if FeatureProvider is nil
func (api *evaluationAPI) setNamedProvider(clientName string, provider FeatureProvider, async bool) error {
// setProvider sets a provider with client domain. Returns an error if FeatureProvider is nil
func (api *evaluationAPI) setNamedProvider(clientDomain string, provider FeatureProvider, async bool) error {
api.mu.Lock()
defer api.mu.Unlock()

Expand All @@ -81,15 +81,15 @@ func (api *evaluationAPI) setNamedProvider(clientName string, provider FeaturePr

// Initialize new named provider and shutdown the old one
// Provider update must be non-blocking, hence initialization & shutdown happens concurrently
oldProvider := api.namedProviders[clientName]
api.namedProviders[clientName] = provider
oldProvider := api.namedProviders[clientDomain]
api.namedProviders[clientDomain] = provider

err := api.initNewAndShutdownOld(provider, oldProvider, async)
if err != nil {
return err
}

err = api.eventExecutor.registerNamedEventingProvider(clientName, provider)
err = api.eventExecutor.registerNamedEventingProvider(clientDomain, provider)
if err != nil {
return err
}
Expand Down Expand Up @@ -159,14 +159,14 @@ func (api *evaluationAPI) getHooks() []Hook {
}

// forTransaction is a helper to retrieve transaction(flag evaluation) scoped operators.
// Returns the default FeatureProvider if no provider mapping exist for the given client name.
func (api *evaluationAPI) forTransaction(clientName string) (FeatureProvider, []Hook, EvaluationContext) {
// Returns the default FeatureProvider if no provider mapping exist for the given client domain.
func (api *evaluationAPI) forTransaction(clientDomain string) (FeatureProvider, []Hook, EvaluationContext) {
api.mu.RLock()
defer api.mu.RUnlock()

var provider FeatureProvider

provider = api.namedProviders[clientName]
provider = api.namedProviders[clientDomain]
if provider == nil {
provider = api.defaultProvider
}
Expand Down
14 changes: 10 additions & 4 deletions openfeature/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,16 @@ func NewClientMetadata(name string) ClientMetadata {
}

// Name returns the client's name
// Deprecated: Name() exists for historical compatibility, use Domain() instead.
func (cm ClientMetadata) Name() string {
return cm.name
}

// Domain returns the client's domain
func (cm ClientMetadata) Domain() string {
return cm.name
}

// Client implements the behaviour required of an openfeature client
type Client struct {
mx sync.RWMutex
Expand All @@ -67,9 +73,9 @@ type Client struct {
var _ IClient = (*Client)(nil)

// NewClient returns a new Client. Name is a unique identifier for this client
func NewClient(name string) *Client {
func NewClient(domain string) *Client {
return &Client{
metadata: ClientMetadata{name: name},
metadata: ClientMetadata{name: domain},
hooks: []Hook{},
evaluationContext: EvaluationContext{},
logger: globalLogger,
Expand Down Expand Up @@ -100,12 +106,12 @@ func (c *Client) AddHooks(hooks ...Hook) {

// AddHandler allows to add Client level event handler
func (c *Client) AddHandler(eventType EventType, callback EventCallback) {
addClientHandler(c.metadata.Name(), eventType, callback)
addClientHandler(c.metadata.Domain(), eventType, callback)
}

// RemoveHandler allows to remove Client level event handler
func (c *Client) RemoveHandler(eventType EventType, callback EventCallback) {
removeClientHandler(c.metadata.Name(), eventType, callback)
removeClientHandler(c.metadata.Domain(), eventType, callback)
}

// SetEvaluationContext sets the client's evaluation context
Expand Down
4 changes: 2 additions & 2 deletions openfeature/client_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import (

func ExampleNewClient() {
client := openfeature.NewClient("example-client")
fmt.Printf("Client Name: %s", client.Metadata().Name())
// Output: Client Name: example-client
fmt.Printf("Client Domain: %s", client.Metadata().Domain())
// Output: Client Domain: example-client
}

func ExampleClient_BooleanValue() {
Expand Down
8 changes: 4 additions & 4 deletions openfeature/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,15 @@ func TestRequirement_1_2_1(t *testing.T) {
}

// The client interface MUST define a `metadata` member or accessor,
// containing an immutable `name` field or accessor of type string,
// which corresponds to the `name` value supplied during client creation.
// containing an immutable `domain` field or accessor of type string,
// which corresponds to the `domain` value supplied during client creation.
func TestRequirement_1_2_2(t *testing.T) {
defer t.Cleanup(initSingleton)
clientName := "test-client"
client := NewClient(clientName)

if client.Metadata().Name() != clientName {
t.Errorf("client name not initiated as expected, got %s, want %s", client.Metadata().Name(), clientName)
if client.Metadata().Domain() != clientName {
t.Errorf("client domain not initiated as expected, got %s, want %s", client.Metadata().Domain(), clientName)
}
}

Expand Down
14 changes: 7 additions & 7 deletions openfeature/event_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ func newEventExecutor(logger logr.Logger) *eventExecutor {
return &executor
}

// scopedCallback is a helper struct to hold client name associated callbacks.
// Here, the scope correlates to the client and provider name
// scopedCallback is a helper struct to hold client domain associated callbacks.
// Here, the scope correlates to the client and provider domain
type scopedCallback struct {
scope string
callbacks map[EventType][]EventCallback
Expand Down Expand Up @@ -111,24 +111,24 @@ func (e *eventExecutor) removeApiHandler(t EventType, c EventCallback) {
}

// registerClientHandler registers a client level handler
func (e *eventExecutor) registerClientHandler(clientName string, t EventType, c EventCallback) {
func (e *eventExecutor) registerClientHandler(clientDomain string, t EventType, c EventCallback) {
e.mu.Lock()
defer e.mu.Unlock()

_, ok := e.scopedRegistry[clientName]
_, ok := e.scopedRegistry[clientDomain]
if !ok {
e.scopedRegistry[clientName] = newScopedCallback(clientName)
e.scopedRegistry[clientDomain] = newScopedCallback(clientDomain)
}

registry := e.scopedRegistry[clientName]
registry := e.scopedRegistry[clientDomain]

if registry.callbacks[t] == nil {
registry.callbacks[t] = []EventCallback{c}
} else {
registry.callbacks[t] = append(registry.callbacks[t], c)
}

reference, ok := e.namedProviderReference[clientName]
reference, ok := e.namedProviderReference[clientDomain]
if !ok {
// fallback to default
reference = e.defaultProviderReference
Expand Down
10 changes: 5 additions & 5 deletions openfeature/event_executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ func TestEventHandler_RegisterUnregisterEventProvider(t *testing.T) {
t.Error("implementation should register default eventing provider")
}

err = executor.registerNamedEventingProvider("name", eventingProvider)
err = executor.registerNamedEventingProvider("domain", eventingProvider)
if err != nil {
t.Fatal(err)
}

if _, ok := executor.namedProviderReference["name"]; !ok {
if _, ok := executor.namedProviderReference["domain"]; !ok {
t.Errorf("implementation should register named eventing provider")
}
})
Expand Down Expand Up @@ -137,7 +137,7 @@ func TestEventHandler_Eventing(t *testing.T) {
eventingImpl,
}

// associated to client name
// associated to client domain
associatedName := "providerForClient"

err := SetNamedProviderAndWait(associatedName, eventingProvider)
Expand Down Expand Up @@ -220,7 +220,7 @@ func TestEventHandler_clientAssociation(t *testing.T) {
t.Fatal(err)
}

// named provider(associated to name someClient)
// named provider(associated to domain someClient)
err = SetNamedProviderAndWait("someClient", struct {
FeatureProvider
EventHandler
Expand Down Expand Up @@ -672,7 +672,7 @@ func TestEventHandler_ProviderReadiness(t *testing.T) {
}
})

t.Run("for name associated handler", func(t *testing.T) {
t.Run("for domain associated handler", func(t *testing.T) {
defer t.Cleanup(initSingleton)

readyEventingProvider := struct {
Expand Down
24 changes: 12 additions & 12 deletions openfeature/openfeature.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,16 @@ func SetProviderAndWait(provider FeatureProvider) error {
return api.setProvider(provider, false)
}

// SetNamedProvider sets a provider mapped to the given Client name. Provider initialization is asynchronous and
// SetNamedProvider sets a provider mapped to the given Client domain. Provider initialization is asynchronous and
// status can be checked from provider status
func SetNamedProvider(clientName string, provider FeatureProvider) error {
return api.setNamedProvider(clientName, provider, true)
func SetNamedProvider(clientDomain string, provider FeatureProvider) error {
return api.setNamedProvider(clientDomain, provider, true)
}

// SetNamedProviderAndWait sets a provider mapped to the given Client name and waits for its initialization.
// SetNamedProviderAndWait sets a provider mapped to the given Client domain and waits for its initialization.
// Returns an error if initialization cause error
func SetNamedProviderAndWait(clientName string, provider FeatureProvider) error {
return api.setNamedProvider(clientName, provider, false)
func SetNamedProviderAndWait(clientDomain string, provider FeatureProvider) error {
return api.setNamedProvider(clientDomain, provider, false)
}

// SetEvaluationContext sets the global evaluation context.
Expand Down Expand Up @@ -67,8 +67,8 @@ func AddHandler(eventType EventType, callback EventCallback) {
}

// addClientHandler is a helper for Client to add an event handler
func addClientHandler(name string, t EventType, c EventCallback) {
api.eventExecutor.registerClientHandler(name, t, c)
func addClientHandler(domain string, t EventType, c EventCallback) {
api.eventExecutor.registerClientHandler(domain, t, c)
}

// RemoveHandler allows to remove API level event handler
Expand All @@ -77,8 +77,8 @@ func RemoveHandler(eventType EventType, callback EventCallback) {
}

// removeClientHandler is a helper for Client to add an event handler
func removeClientHandler(name string, t EventType, c EventCallback) {
api.eventExecutor.removeClientHandler(name, t, c)
func removeClientHandler(domain string, t EventType, c EventCallback) {
api.eventExecutor.removeClientHandler(domain, t, c)
}

// getAPIEventRegistry is a helper for testing
Expand Down Expand Up @@ -122,6 +122,6 @@ func globalLogger() logr.Logger {

// forTransaction is a helper to retrieve transaction scoped operators by Client.
// Here, transaction means a flag evaluation.
func forTransaction(clientName string) (FeatureProvider, []Hook, EvaluationContext) {
return api.forTransaction(clientName)
func forTransaction(clientDomain string) (FeatureProvider, []Hook, EvaluationContext) {
return api.forTransaction(clientDomain)
}
14 changes: 7 additions & 7 deletions openfeature/openfeature_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ func TestRequirement_1_1_2_3(t *testing.T) {
}
})

t.Run("ignore shutdown for multiple references - name client bound", func(t *testing.T) {
t.Run("ignore shutdown for multiple references - domain client bound", func(t *testing.T) {
defer t.Cleanup(initSingleton)

// setup
Expand Down Expand Up @@ -393,8 +393,8 @@ func TestRequirement_1_1_2_4(t *testing.T) {
})
}

// The `API` MUST provide a function to bind a given `provider` to one or more client `name`s.
// If the client-name already has a bound provider, it is overwritten with the new mapping.
// The `API` MUST provide a function to bind a given `provider` to one or more client `domain`s.
// If the client-domain already has a bound provider, it is overwritten with the new mapping.
func TestRequirement_1_1_3(t *testing.T) {
defer t.Cleanup(initSingleton)

Expand Down Expand Up @@ -445,7 +445,7 @@ func TestRequirement_1_1_3(t *testing.T) {
t.Errorf("expected %s, but got %s", "providerB", providerA.Metadata().Name)
}

// Validate overriding: If the client-name already has a bound provider, it is overwritten with the new mapping.
// Validate overriding: If the client-domain already has a bound provider, it is overwritten with the new mapping.

providerB2 := NewMockFeatureProvider(ctrl)
providerB2.EXPECT().Metadata().Return(Metadata{Name: "providerB2"}).AnyTimes()
Expand Down Expand Up @@ -495,7 +495,7 @@ func TestRequirement_1_1_5(t *testing.T) {
}

// The `API` MUST provide a function for creating a `client` which accepts the following options:
// - name (optional): A logical string identifier for the client.
// - domain (optional): A logical string identifier for the client.
func TestRequirement_1_1_6(t *testing.T) {
defer t.Cleanup(initSingleton)
NewClient("test-client")
Expand Down Expand Up @@ -563,7 +563,7 @@ func TestRequirement_EventCompliance(t *testing.T) {
registry := getClientRegistry(clientName)

if registry == nil {
t.Fatalf("no event handler registry present for client name %s", clientName)
t.Fatalf("no event handler registry present for client domain %s", clientName)
}

if len(registry.callbacks[ProviderReady]) < 1 {
Expand Down Expand Up @@ -661,7 +661,7 @@ func TestRequirement_EventCompliance(t *testing.T) {

// Non-spec bound validations

// If there is no client name bound provider, then return the default provider
// If there is no client domain bound provider, then return the default provider
func TestDefaultClientUsage(t *testing.T) {
defer t.Cleanup(initSingleton)

Expand Down

0 comments on commit a9e19dd

Please sign in to comment.