Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs(specification): Flag set specification proposal #2734

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
200 changes: 200 additions & 0 deletions website/src/pages/specification/20241027-flagsets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
---
title: Flag sets
Description: Flag sets are a way to group flags together.
---

# Flag sets

| | |
|----------------------|---------------------------------------------------|
| **Creation Date** | 27/10/2024 |
| **Last Update Date** | 27/10/2024 |
| **Authors** | Thomas Poignant |
| **Status** | ![draft](https://img.shields.io/badge/-draft-red) |

## Definition

> A collection of related flags. This grouping helps organize feature flags based on their intended use, facilitating easier management and deployment.

_Source: [Openfeature glossary](https://openfeature.dev/specification/glossary/#flag-set)._


## Context

GO Feature Flag is supporting both flag evaluation for client and server-side.
While the server side evaluations are evaluating flags 1 by 1, the client-side providers are evaluating flags in bulk.
The main reason why client are evaluating flags in bulk is because in the client-side the evaluation context is not changing
for every evaluation, so in order to limit the number of requests to the relay proxy, we are evaluating flags in bulk and keeping
them in memory inside the different client providers _(`web`, `iOS` and `android`)_ for OpenFeature.

### Sequence diagram of a client-side evaluation

```mermaid
sequenceDiagram
participant app as Application
participant Client as Openfeature SDK
participant Provider as GO Feature Flag Provider
participant RelayProxy
app->>Client: SetContext(myEvaluationCtx)
Client->>Provider: init()
Provider->>RelayProxy: call /ofrep/v1/evaluate/flags
RelayProxy-->>Provider: return all evaluation responses
Provider->>Provider: Cache evaluation responses
Provider-->>Client:
Client -->> app:

app->>Client: getBooleanValue(...)
Client->>Provider: resolveBooleanValue(...)
Provider-->>Client: evaluation response from cache for the flag
Client -->> app: flag value
```
:::note
For simplicity, we are not showing the cache mechanism in the sequence diagram, and how the cache is updated in case of flag configuration changes.
:::

### Why introducing flag sets?

As of today, in the client-side paradigm, we are evaluating all the flags available in GO Feature Flag based on the received evaluation context.
This means that we are evaluating all the flags available in the project, but in some cases, we might want to evaluate only a subset of the flags.

**When do we want to use flag sets?**
- We have multiple teams using the same GO Feature Flag instance, and we want to separate the flags evaluated by each team.
- We have different platforms using GO Feature Flag, and we want to limit which flags are evaluated by each platform.
- We want to give access to a list of flags to a specific user group.
- We want to allow 2 flags with the same name if they are used by different teams or platforms.

**For all those points, as of today the only way to achieve this is to run multiple instances of GO Feature Flag, which is not ideal.**

## Requirements

- A flag can be part of only 1 flag set.
- Flag name are unique within a flag set, but not across flag sets.
- GO Feature Flag should have a `default` flag set for users not using the flag sets feature _(the behaviours should be exactly the same as of today if the feature is not used)_.
- When calling the bulk evaluation APIs (`/ofrep/v1/evaluate/flags` or `/v1/allflags`), we will determine which flag set to evaluate based on API key used.
- The bulk evaluation APIs should evaluate only 1 flag set at a time _(to avoid collision in flag names)_.
- It should be able to specify which flag set to evaluate when calling the evaluation APIs. _(e.g. `/ofrep/v1/evaluate/flags` or `/ofrep/v1/evaluate/flags/{flag_key}`)_.
- Ideally we should be able to know which flag set to used based on the API Key used.
- If GOFF is configured to be public, we should be able to specify the flag set to evaluate in the request _(with a specific header ex:`goff-flag-set`)_.
- In the providers, we should be able to specify the flag set to evaluate directly in the constructor _(by providing an API Key, or the name of the flag set)_.
- Admin API Keys, should be able to evaluate all the flag sets _(to be able to see all the flags available in the project)_. If none specified in the request, the default flag set should be evaluated.


## Out of scope for now
- Dynamic flag sets based on the evaluation context _([as mentioned in this slack message](https://gophers.slack.com/archives/C029TH8KDFG/p1732703075509229))_.
- Single retrievers for multiple flag sets _(as proposed in https://github.com/thomaspoignant/go-feature-flag/issues/2314)_.

_Even if out of scope for now, those are interesting options that we may want to implement later._

## Proposed solution

### Solution 1: 1 flag set per file

In this solution we consider that we need at least one file per flag set.
All the flags retrieved by a `retriever` _(aka in the same file)_ will be associated to the same flag set.

We can specify the flag set name in the configuration file.
As of today, we can still have multiple files for 1 flag set _(by specifying the same `flagSet` name in each file)_,
and we will have the same mechanism as of today _(with flag overridden in case of flag name collision)_.

#### Flags configuration file
```yaml
# config-file1.goff.yaml
# Syntax used in this example is just for the sake of the example, it is not the final syntax.
flagSet: flagset-teamA

featureA-enabled:
variations:
enabled: true
disabled: false
defaultRule:
variation: enabled
```
```yaml
# config-file2.goff.yaml
flagSet: flagset-teamB

featureA-enabled:
variations:
enabled: true
disabled: false
defaultRule:
variation: enabled
```

#### Relay-proxy configuration example
```yaml
# ...
retrievers:
- kind: file
path: config-file1.goff.yaml
- kind: file
path: config-file2.goff.yaml

authorizedKeys:
evaluation:
- apikey1 # owner: userID1
- apikey2 # owner: userID2
admin:
- apikey3
Comment on lines +106 to +111
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 I am not sure how to configure the relation between the API keys and the flag sets, all proposals are welcome on this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just went through the API keys configuration docs and it looks like it accept any string. Since it's an array it can be organized so that the available flag sets for a specific api key are declared as well in the config file.

Another approach would be to generate something like JWTs to be used as API keys and encode flag set access data as claims. This path would be much more complex

Copy link
Owner Author

@thomaspoignant thomaspoignant Nov 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just went through the API keys configuration docs and it looks like it accept any string. Since it's an array it can be organized so that the available flag sets for a specific api key are declared as well in the config file.

Yes it can be anything here.
I am just not sure how to represent the configuration without breaking changes for existing config 🤔.

Another approach would be to generate something like JWTs to be used as API keys and encode flag set access data as claims. This path would be much more complex

I was also thinking about that, but it could be an evolution for a later stage.

```

**PRO**
- It is simple to configure a flag set by putting all the flags at the same place.
- It is easy to understand which flags are part of a flag set.
- It is easy to give ownership of a flag set to a team, by giving them access to the file.

**CON**
- If we want the same flag to be part of multiple flag sets, we need to duplicate the flag in multiple files.

### Solution 2: specify the flag set in the retriever configuration

In this solution we consider that we need at least one file per flag set.
All the flags retrieved by a `retriever` _(aka in the same file)_ will be associated to the same flag set.

We associate the flag set to the retriever in the configuration,
#### Flags configuration file
```yaml
# config-file1.goff.yaml
featureA-enabled:
variations:
enabled: true
disabled: false
defaultRule:
variation: enabled
```
```yaml
# config-file2.goff.yaml
featureA-enabled:
variations:
enabled: true
disabled: false
defaultRule:
variation: enabled
```

#### Relay-proxy configuration example
```yaml
# ...
retrievers:
- kind: file
path: config-file1.goff.yaml
flagSet: flagset-teamA
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think by doing it this way it ends up more flexible.

Since it becomes part of the retriever config and not the flag definition itself it won't be necessary to come up with different approaches to retrievers like Redis and MongoDB.

Does it make sense?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes you are right, it makes more sense to put it outside of the configuration and move it to the retriever configuration.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little confused how this would work in practice though? If tomorrow a new team comes in and they need their own flag set, then GOFF would need to be redeployed again with a new configuration for another retriever. In our companies' deployment/FF solution, we're hoping each development project in each team is an individual flag set to avoid possible conflict, which means every new project on every team that's created would require a production redeployment of go-feature-flag on our side. Obviously this isn't really maintainable, which is what our feature request aimed to solve.

Disregarding the feature request previously, would another solution such as having the flagset key defined per flag work? Each flag could have a flagset key that if not provided would be default (keeping original functionality), otherwise would set the flag as being owned by that flagset. The issue here is flags still have the same name in their source (S3, MongoDB, Redis, etc.), but this doesn't have to be an issue for goff as when it loads flags into its memory it can use the flagset to distinguish flags internally.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little confused how this would work in practice though? If tomorrow a new team comes in and they need their own flag set, then GOFF would need to be redeployed again with a new configuration for another retriever. In our companies' deployment/FF solution, we're hoping each development project in each team is an individual flag set to avoid possible conflict, which means every new project on every team that's created would require a production redeployment of go-feature-flag on our side. Obviously this isn't really maintainable, which is what our #2314 aimed to solve.

@iisharankov your use case is still in my mind but I want to first introduce the notion of flag set and later address your usecase.
All the proposals are still valid with your use case, it will just need to have some kind of meta retriever that will be able to automatically create a flag set.

I place your need for now in the Out of scope section, but as soon as we have the flag set ready we will be able to work on this too.

Disregarding the feature request previously, would another solution such as having the flagset key defined per flag work? Each flag could have a flagset key that if not provided would be default (keeping original functionality), otherwise would set the flag as being owned by that flagset. The issue here is flags still have the same name in their source (S3, MongoDB, Redis, etc.), but this doesn't have to be an issue for goff as when it loads flags into its memory it can use the flagset to distinguish flags internally.

Yes that was my original idea, but we will have limits because of unicity of flag name here.
See the example in my comments here: #2734 (comment)

- kind: file
path: config-file2.goff.yaml
flagSet: flagset-teamB

authorizedKeys:
evaluation:
- apikey1 # owner: userID1
- apikey2 # owner: userID2
admin:
- apikey3
Comment on lines +162 to +167
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 I am not sure how to configure the relation between the API keys and the flag sets, all proposals are welcome on this.

```
### Solution 3
:::note
Feel free to propose other solutions here.
:::

## Decision

## Consequences
Loading