Skip to content

Commit 64037d3

Browse files
emyllerkhvn26
andauthored
feat: Deliver the deployed API version to an HTTP response header (#59)
* feat: Deliver the deployed API version to an HTTP response header * Fix tests * Improve typing * Fix tests again ✨ * Fix HTTP header name * Improve tests * Apply the linter's take on my code * Refactor reading the package version into the HTTP header * Fix an unrelated test * Revert "Fix an unrelated test" This reverts commit 7dbd9a5. * Improve function name * Update src/common/core/utils.py Co-authored-by: Kim Gustyr <[email protected]> * Update src/common/core/utils.py Co-authored-by: Kim Gustyr <[email protected]> --------- Co-authored-by: Kim Gustyr <[email protected]>
1 parent 74c73b4 commit 64037d3

File tree

4 files changed

+117
-8
lines changed

4 files changed

+117
-8
lines changed

src/common/core/middleware.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from typing import Callable
2+
3+
from django.http import HttpRequest, HttpResponse
4+
5+
from common.core.utils import get_version
6+
7+
8+
class APIResponseVersionHeaderMiddleware:
9+
"""
10+
Middleware to add the API version to the response headers
11+
"""
12+
13+
def __init__(
14+
self,
15+
get_response: Callable[[HttpRequest], HttpResponse],
16+
) -> None:
17+
self.get_response = get_response
18+
19+
def __call__(self, request: HttpRequest) -> HttpResponse:
20+
response = self.get_response(request)
21+
response.headers["Flagsmith-Version"] = get_version()
22+
return response

src/common/core/utils.py

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,22 @@ class SelfHostedData(TypedDict):
1717
has_logins: bool
1818

1919

20+
VersionManifest = TypedDict(
21+
"VersionManifest",
22+
{
23+
".": str, # This key is used to store the version of the package itself
24+
},
25+
)
26+
27+
2028
class VersionInfo(TypedDict):
2129
ci_commit_sha: str
2230
image_tag: str
2331
has_email_provider: bool
2432
is_enterprise: bool
2533
is_saas: bool
2634
self_hosted_data: SelfHostedData | None
27-
package_versions: NotRequired[dict[str, str]]
35+
package_versions: NotRequired[VersionManifest]
2836

2937

3038
@lru_cache()
@@ -55,7 +63,7 @@ def has_email_provider() -> bool:
5563

5664

5765
def get_version_info() -> VersionInfo:
58-
"""Reads the version info baked into src folder of the docker container"""
66+
"""Returns the version information for the current deployment"""
5967
_is_saas = is_saas()
6068
version_json: VersionInfo = {
6169
"ci_commit_sha": get_file_contents("./CI_COMMIT_SHA") or UNKNOWN,
@@ -66,12 +74,9 @@ def get_version_info() -> VersionInfo:
6674
"self_hosted_data": None,
6775
}
6876

69-
manifest_versions_content = get_file_contents(VERSIONS_INFO_FILE_LOCATION)
70-
71-
if manifest_versions_content:
72-
manifest_versions = json.loads(manifest_versions_content)
73-
version_json["package_versions"] = manifest_versions
74-
version_json["image_tag"] = manifest_versions["."]
77+
manifest_versions = get_versions_from_manifest()
78+
version_json["package_versions"] = manifest_versions
79+
version_json["image_tag"] = manifest_versions["."]
7580

7681
if not _is_saas:
7782
user_objects: Manager[AbstractBaseUser] = getattr(get_user_model(), "objects")
@@ -84,6 +89,23 @@ def get_version_info() -> VersionInfo:
8489
return version_json
8590

8691

92+
def get_version() -> str:
93+
"""Return the version number of the current deployment"""
94+
manifest_versions = get_versions_from_manifest()
95+
return manifest_versions.get(".", UNKNOWN)
96+
97+
98+
@lru_cache()
99+
def get_versions_from_manifest() -> VersionManifest:
100+
"""Read the version info from the manifest file"""
101+
raw_content = get_file_contents(VERSIONS_INFO_FILE_LOCATION)
102+
if not raw_content:
103+
return {".": UNKNOWN}
104+
105+
manifest: VersionManifest = json.loads(raw_content)
106+
return manifest
107+
108+
87109
@lru_cache()
88110
def get_file_contents(file_path: str) -> str | None:
89111
"""Attempts to read a file from the filesystem and return the contents"""
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from pytest_mock import MockerFixture
2+
3+
from common.core import middleware as middleware_module
4+
5+
6+
def test_APIResponseVersionHeaderMiddleware__adds_version_header(
7+
mocker: MockerFixture,
8+
) -> None:
9+
# Given
10+
request = mocker.Mock()
11+
response = mocker.Mock(headers={})
12+
get_response = mocker.Mock(return_value=response)
13+
middleware = middleware_module.APIResponseVersionHeaderMiddleware(get_response)
14+
get_version = mocker.patch.object(
15+
middleware_module,
16+
"get_version",
17+
return_value="v1.2.3",
18+
)
19+
20+
# When
21+
result = middleware(request)
22+
23+
# Then
24+
assert result == response
25+
assert response.headers["Flagsmith-Version"] == "v1.2.3"
26+
get_response.assert_called_once_with(request)
27+
get_version.assert_called_once_with()

tests/unit/common/core/test_utils.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77

88
from common.core.utils import (
99
get_file_contents,
10+
get_version,
1011
get_version_info,
12+
get_versions_from_manifest,
1113
has_email_provider,
1214
is_enterprise,
1315
is_oss,
@@ -21,6 +23,7 @@
2123
def clear_lru_caches() -> Generator[None, None, None]:
2224
yield
2325
get_file_contents.cache_clear()
26+
get_versions_from_manifest.cache_clear()
2427
has_email_provider.cache_clear()
2528
is_enterprise.cache_clear()
2629
is_saas.cache_clear()
@@ -89,6 +92,7 @@ def test_get_version_info_with_missing_files(fs: FakeFilesystem) -> None:
8992
"has_email_provider": False,
9093
"is_enterprise": True,
9194
"is_saas": False,
95+
"package_versions": {".": "unknown"},
9296
"self_hosted_data": {
9397
"has_logins": False,
9498
"has_users": False,
@@ -145,3 +149,37 @@ def test_get_version_info__email_config_disabled__return_expected(
145149

146150
# Then
147151
assert result["has_email_provider"] is False
152+
153+
154+
def test_get_version__valid_file_contents__returns_version_number(
155+
fs: FakeFilesystem,
156+
) -> None:
157+
# Given
158+
fs.create_file("./.versions.json", contents='{".": "v1.2.3"}')
159+
160+
# When
161+
result = get_version()
162+
163+
# Then
164+
assert result == "v1.2.3"
165+
166+
167+
@pytest.mark.parametrize(
168+
"manifest_contents",
169+
[
170+
'{"foo": "bar"}',
171+
"",
172+
],
173+
)
174+
def test_get_version__invalid_file_contents__returns_unknown(
175+
fs: FakeFilesystem,
176+
manifest_contents: str,
177+
) -> None:
178+
# Given
179+
fs.create_file("./.versions.json", contents=manifest_contents)
180+
181+
# When
182+
result = get_version()
183+
184+
# Then
185+
assert result == "unknown"

0 commit comments

Comments
 (0)