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

feat: integrate flagsmith client into API layer #2447

Merged
merged 18 commits into from
Sep 4, 2023
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
khvn26 marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -903,3 +904,13 @@
DOMAIN_OVERRIDE = env.str("FLAGSMITH_DOMAIN", "")
# Used when no Django site is specified.
DEFAULT_DOMAIN = "app.flagsmith.com"

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