From a9e19dd5ec52bf8d84a8b2e2f6d263d841059d61 Mon Sep 17 00:00:00 2001 From: Gunish Matta <33680363+gunishmatta@users.noreply.github.com> Date: Fri, 17 May 2024 01:41:19 +0530 Subject: [PATCH] feat: Implement domain scoping (#261) * added support for domains Signed-off-by: gunishmatta * added support for domains Signed-off-by: gunishmatta * 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 * 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 * Update openfeature/client.go Co-authored-by: Michael Beemer Signed-off-by: Gunish Matta <33680363+gunishmatta@users.noreply.github.com> Signed-off-by: gunishmatta * Update openfeature/client.go Co-authored-by: Michael Beemer Signed-off-by: Gunish Matta <33680363+gunishmatta@users.noreply.github.com> Signed-off-by: gunishmatta * code review changes Signed-off-by: gunishmatta * code review changes Signed-off-by: gunishmatta * code review changes Signed-off-by: gunishmatta * code review changes Signed-off-by: gunishmatta * feat: Added domain scoping #261 Please enter the commit message for your chanes. Lines starting Author: Gunish Matta Signed-off-by: gunishmatta * 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 * chore: bump Go to version 1.20 (#255) * chore: bump Go to version 1.21 Signed-off-by: odubajDT * update readme with go version and note Signed-off-by: Kavindu Dodanduwa * Update README.md Co-authored-by: Michael Beemer Signed-off-by: Kavindu Dodanduwa --------- Signed-off-by: odubajDT Signed-off-by: Kavindu Dodanduwa Signed-off-by: Kavindu Dodanduwa Co-authored-by: Kavindu Dodanduwa Co-authored-by: Kavindu Dodanduwa Co-authored-by: Michael Beemer Signed-off-by: gunishmatta * code review changes Signed-off-by: gunishmatta * code review changes Signed-off-by: gunishmatta * code review changes Signed-off-by: gunishmatta * code review changes Signed-off-by: gunishmatta * 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 * 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 * 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 --------- Signed-off-by: Michael Beemer Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Michael Beemer Signed-off-by: gunishmatta * 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 * 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 * Revert "code review changes" This reverts commit 3472ee897827542991935111b8895e8c18fa6bec. Signed-off-by: gunishmatta * code review changes Signed-off-by: gunishmatta --------- Signed-off-by: gunishmatta Signed-off-by: Gunish Matta <33680363+gunishmatta@users.noreply.github.com> Signed-off-by: odubajDT Signed-off-by: Kavindu Dodanduwa Signed-off-by: Kavindu Dodanduwa Signed-off-by: Michael Beemer Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Michael Beemer Co-authored-by: Dave Henderson Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: odubajDT <93584209+odubajDT@users.noreply.github.com> Co-authored-by: Kavindu Dodanduwa Co-authored-by: Kavindu Dodanduwa --- README.md | 28 +++++++++++++--------------- openfeature/api.go | 16 ++++++++-------- openfeature/client.go | 14 ++++++++++---- openfeature/client_example_test.go | 4 ++-- openfeature/client_test.go | 8 ++++---- openfeature/event_executor.go | 14 +++++++------- openfeature/event_executor_test.go | 10 +++++----- openfeature/openfeature.go | 24 ++++++++++++------------ openfeature/openfeature_test.go | 14 +++++++------- 9 files changed, 68 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index 290740d4..37fc7ebd 100644 --- a/README.md +++ b/README.md @@ -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. | Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌ @@ -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 @@ -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" @@ -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. diff --git a/openfeature/api.go b/openfeature/api.go index 155c5e2b..fc04de3e 100644 --- a/openfeature/api.go +++ b/openfeature/api.go @@ -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() @@ -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 } @@ -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 } diff --git a/openfeature/client.go b/openfeature/client.go index 4bb83f21..4d54d007 100644 --- a/openfeature/client.go +++ b/openfeature/client.go @@ -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 @@ -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, @@ -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 diff --git a/openfeature/client_example_test.go b/openfeature/client_example_test.go index 27bc08c9..f72290e0 100644 --- a/openfeature/client_example_test.go +++ b/openfeature/client_example_test.go @@ -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() { diff --git a/openfeature/client_test.go b/openfeature/client_test.go index d02d8bdb..debcc1dd 100644 --- a/openfeature/client_test.go +++ b/openfeature/client_test.go @@ -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) } } diff --git a/openfeature/event_executor.go b/openfeature/event_executor.go index b3df6cb3..53843972 100644 --- a/openfeature/event_executor.go +++ b/openfeature/event_executor.go @@ -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 @@ -111,16 +111,16 @@ 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} @@ -128,7 +128,7 @@ func (e *eventExecutor) registerClientHandler(clientName string, t EventType, c 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 diff --git a/openfeature/event_executor_test.go b/openfeature/event_executor_test.go index d4783e99..fec36577 100644 --- a/openfeature/event_executor_test.go +++ b/openfeature/event_executor_test.go @@ -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") } }) @@ -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) @@ -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 @@ -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 { diff --git a/openfeature/openfeature.go b/openfeature/openfeature.go index 40cf61a9..69b02964 100644 --- a/openfeature/openfeature.go +++ b/openfeature/openfeature.go @@ -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. @@ -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 @@ -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 @@ -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) } diff --git a/openfeature/openfeature_test.go b/openfeature/openfeature_test.go index 7fa8d0c2..a41cbc29 100644 --- a/openfeature/openfeature_test.go +++ b/openfeature/openfeature_test.go @@ -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 @@ -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) @@ -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() @@ -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") @@ -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 { @@ -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)