Skip to content

Commit

Permalink
feat: integrate flagsmith client into API layer (#2447)
Browse files Browse the repository at this point in the history
Co-authored-by: Kim Gustyr <[email protected]>
  • Loading branch information
matthewelwell and khvn26 authored Sep 4, 2023
1 parent 316ac80 commit e71efbb
Show file tree
Hide file tree
Showing 15 changed files with 467 additions and 1 deletion.
41 changes: 41 additions & 0 deletions .github/workflows/update-flagsmith-environment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Update Flagsmith Defaults

on:
schedule:
- cron: 0 8 * * *

defaults:
run:
working-directory: api

jobs:
update_server_defaults:
runs-on: ubuntu-latest
name: Update API Flagsmith Defaults
env:
FLAGSMITH_ON_FLAGSMITH_SERVER_API_URL: https://edge.api.flagsmith.com/api/v1
FLAGSMITH_ON_FLAGSMITH_SERVER_KEY: ${{ secrets.FLAGSMITH_ON_FLAGSMITH_SERVER_KEY }}

steps:
- uses: actions/checkout@v3

- name: Set up Python 3.11
uses: actions/setup-python@v4
with:
python-version: 3.11
cache: pip

- name: Install Dependencies
run: make install

- name: Update defaults
run: python manage.py updateflagsmithenvironment

- name: Create Pull Request
uses: peter-evans/create-pull-request@v5
with:
commit-message: Update API Flagsmith Defaults
branch: chore/update-api-flagsmith-environment
delete-branch: true
title: 'chore: update Flagsmith environment document'
labels: api
11 changes: 11 additions & 0 deletions api/app/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@
"integrations.slack",
"integrations.webhook",
"integrations.dynatrace",
"integrations.flagsmith",
# Rate limiting admin endpoints
"axes",
"telemetry",
Expand Down Expand Up @@ -919,3 +920,13 @@

# Limit the count of password reset emails that can be dispatched within the `PASSWORD_RESET_EMAIL_COOLDOWN` timeframe.
MAX_PASSWORD_RESET_EMAILS = env.int("MAX_PASSWORD_RESET_EMAILS", 5)

FLAGSMITH_ON_FLAGSMITH_SERVER_OFFLINE_MODE = env.bool(
"FLAGSMITH_ON_FLAGSMITH_SERVER_OFFLINE_MODE", default=True
)
FLAGSMITH_ON_FLAGSMITH_SERVER_KEY = env(
"FLAGSMITH_ON_FLAGSMITH_SERVER_KEY", default=None
)
FLAGSMITH_ON_FLAGSMITH_SERVER_API_URL = env(
"FLAGSMITH_ON_FLAGSMITH_SERVER_API_URL", default=FLAGSMITH_ON_FLAGSMITH_API_URL
)
Empty file.
54 changes: 54 additions & 0 deletions api/integrations/flagsmith/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""
Wrapper module for the flagsmith client to implement singleton behaviour and provide some
additional logic by wrapping the client.
Usage:
```
environment_flags = get_client().get_environment_flags()
identity_flags = get_client().get_identity_flags()
```
Possible extensions:
- Allow for multiple clients?
"""
import typing

from django.conf import settings
from flagsmith import Flagsmith
from flagsmith.offline_handlers import LocalFileHandler

from integrations.flagsmith.exceptions import FlagsmithIntegrationError
from integrations.flagsmith.flagsmith_service import ENVIRONMENT_JSON_PATH

_flagsmith_client: typing.Optional[Flagsmith] = None


def get_client() -> Flagsmith:
global _flagsmith_client

if not _flagsmith_client:
_flagsmith_client = Flagsmith(**_get_client_kwargs())

return _flagsmith_client


def _get_client_kwargs() -> dict[str, typing.Any]:
_default_kwargs = {"offline_handler": LocalFileHandler(ENVIRONMENT_JSON_PATH)}

if settings.FLAGSMITH_ON_FLAGSMITH_SERVER_OFFLINE_MODE:
return {"offline_mode": True, **_default_kwargs}
elif (
settings.FLAGSMITH_ON_FLAGSMITH_SERVER_KEY
and settings.FLAGSMITH_ON_FLAGSMITH_SERVER_API_URL
):
return {
"environment_key": settings.FLAGSMITH_ON_FLAGSMITH_SERVER_KEY,
"api_url": settings.FLAGSMITH_ON_FLAGSMITH_SERVER_API_URL,
**_default_kwargs,
}

raise FlagsmithIntegrationError(
"Must either use offline mode, or provide "
"FLAGSMITH_ON_FLAGSMITH_SERVER_KEY and FLAGSMITH_ON_FLAGSMITH_SERVER_API_URL."
)
34 changes: 34 additions & 0 deletions api/integrations/flagsmith/data/environment.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"api_key": "masked",
"feature_states": [
{
"feature": {
"id": 51089,
"name": "test",
"type": "STANDARD"
},
"enabled": false,
"django_id": 286268,
"feature_segment": null,
"featurestate_uuid": "ec33a926-0b7e-4eb7-b02b-bf9df2ffa53e",
"feature_state_value": null,
"multivariate_feature_state_values": []
}
],
"id": 0,
"name": "Development",
"project": {
"id": 0,
"name": "Flagsmith API",
"hide_disabled_flags": false,
"organisation": {
"id": 0,
"name": "Flagsmith",
"feature_analytics": false,
"stop_serving_flags": false,
"persist_trait_data": true
},
"segments": []
},
"use_identity_composite_key_for_hashing": true
}
2 changes: 2 additions & 0 deletions api/integrations/flagsmith/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class FlagsmithIntegrationError(Exception):
pass
75 changes: 75 additions & 0 deletions api/integrations/flagsmith/flagsmith_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import json
import os

import requests
from django.conf import settings

from integrations.flagsmith.exceptions import FlagsmithIntegrationError

ENVIRONMENT_JSON_PATH = os.path.join(
os.path.dirname(os.path.realpath(__file__)), "data/environment.json"
)

KEEP_ENVIRONMENT_FIELDS = (
"name",
"feature_states",
"use_identity_composite_key_for_hashing",
)
KEEP_PROJECT_FIELDS = ("name", "organisation", "hide_disabled_flags")
KEEP_ORGANISATION_FIELDS = (
"name",
"feature_analytics",
"stop_serving_flags",
"persist_trait_data",
)


def update_environment_json(environment_key: str = None, api_url: str = None) -> None:
environment_key = environment_key or settings.FLAGSMITH_ON_FLAGSMITH_SERVER_KEY
api_url = api_url or settings.FLAGSMITH_ON_FLAGSMITH_SERVER_API_URL

response = requests.get(
f"{api_url}/environment-document",
headers={"X-Environment-Key": environment_key},
)
if response.status_code != 200:
raise FlagsmithIntegrationError(
f"Couldn't get defaults from Flagsmith. Got {response.status_code} response."
)

environment_json = _get_masked_environment_data(response.json())
with open(ENVIRONMENT_JSON_PATH, "w+") as defaults:
defaults.write(json.dumps(environment_json, indent=2, sort_keys=True))


def _get_masked_environment_data(environment_document: dict) -> dict:
"""
Return a cut down / masked version of the environment
document which can be committed to VCS.
"""

project_json = environment_document.pop("project")
organisation_json = project_json.pop("organisation")

return {
"id": 0,
"api_key": "masked",
**{
k: v
for k, v in environment_document.items()
if k in KEEP_ENVIRONMENT_FIELDS
},
"project": {
"id": 0,
**{k: v for k, v in project_json.items() if k in KEEP_PROJECT_FIELDS},
"organisation": {
"id": 0,
**{
k: v
for k, v in organisation_json.items()
if k in KEEP_ORGANISATION_FIELDS
},
},
"segments": [],
},
}
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from django.core.management import BaseCommand

from integrations.flagsmith.flagsmith_service import update_environment_json


class Command(BaseCommand):
def handle(self, *args, **options):
update_environment_json()
34 changes: 33 additions & 1 deletion api/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ django-ses = "~3.5.0"
django-axes = "~5.32.0"
pydantic = "~1.10.9"
pyngo = "~1.6.0"
flagsmith = "^3.4.0"

[tool.poetry.group.auth-controller.dependencies]
django-multiselectfield = "~0.1.12"
Expand Down
Empty file.
Loading

3 comments on commit e71efbb

@vercel
Copy link

@vercel vercel bot commented on e71efbb Sep 4, 2023

Choose a reason for hiding this comment

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

@vercel
Copy link

@vercel vercel bot commented on e71efbb Sep 4, 2023

Choose a reason for hiding this comment

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

Successfully deployed to the following URLs:

docs – ./docs

docs-flagsmith.vercel.app
docs-git-main-flagsmith.vercel.app
docs.flagsmith.com
docs.bullet-train.io

@vercel
Copy link

@vercel vercel bot commented on e71efbb Sep 4, 2023

Choose a reason for hiding this comment

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

Please sign in to comment.