33# SPDX-License-Identifier: MIT
44from __future__ import annotations
55
6+ import os
7+ import sys
8+ from abc import ABC , abstractmethod
69from functools import cached_property
710from typing import TYPE_CHECKING , Any , Optional
811
12+ from dda .config .constants import AppEnvVars
913from dda .feature_flags .client import DatadogFeatureFlag
14+ from dda .secrets .ssm import fetch_secret_ssm
15+ from dda .secrets .vault import fetch_secret_ci
1016from dda .user .datadog import User
11- from dda .utils .ci import running_in_ci
17+ from dda .utils .platform import get_os_name
1218
1319if TYPE_CHECKING :
1420 from dda .cli .application import Application
@@ -20,10 +26,9 @@ def __init__(self, config: RootConfig) -> None:
2026 super ().__init__ (config )
2127
2228
23- class FeatureFlagManager :
29+ class FeatureFlagManager ( ABC ) :
2430 """
25- A class for querying feature flags. This is available as the
26- [`Application.features`][dda.cli.application.Application.features] property.
31+ A class for querying feature flags.
2732 """
2833
2934 def __init__ (self , app : Application ) -> None :
@@ -34,11 +39,129 @@ def __init__(self, app: Application) -> None:
3439 # the cache will contain the result for the tuple ("test-flag", "entity", (("user", "user1"),)).
3540 self .__cache : dict [tuple [str , str , tuple [tuple [str , str ], ...]], Any ] = {}
3641
42+ def enabled (self , flag : str , * , default : bool = False , scopes : Optional [dict [str , str ]] = None ) -> bool :
43+ entity = self .__entity
44+ base_scopes = self .__base_scopes
45+ if scopes is not None :
46+ base_scopes .update (scopes )
47+
48+ attributes_items = base_scopes .items ()
49+ tuple_attributes = tuple (((key , value ) for key , value in sorted (attributes_items )))
50+
51+ self ._app .display_debug (
52+ f"Checking flag { flag } with targeting key { entity } and targeting attributes { tuple_attributes } "
53+ )
54+ flag_value = self .__check_flag (flag , entity , tuple_attributes )
55+ if flag_value is None :
56+ return default
57+ return flag_value
58+
59+ def __check_flag (self , flag : str , entity : str , scopes : tuple [tuple [str , str ], ...]) -> bool | None :
60+ if self .__client is None :
61+ self .__app .display_debug ("Feature flag client not initialized properly" )
62+ return None
63+
64+ cache_key = (flag , entity , scopes )
65+ if cache_key in self .__cache :
66+ return self .__cache [cache_key ]
67+
68+ flag_value = self .__client .get_flag_value (flag , entity , dict (scopes ))
69+ self .__cache [cache_key ] = flag_value
70+ return flag_value
71+
72+ @abstractmethod
73+ def _get_client_token (self ) -> str | None :
74+ pass
75+
76+ @abstractmethod
77+ def _get_entity (self ) -> str :
78+ pass
79+
80+ @abstractmethod
81+ def _get_base_scopes (self ) -> dict [str , str ]:
82+ pass
83+
84+ @abstractmethod
85+ def _get_client_token (self ) -> str | None :
86+ pass
87+
3788 @cached_property
3889 def __client (self ) -> DatadogFeatureFlag | None :
39- if running_in_ci (): # We do not support feature flags token retrieval in the CI yet.
90+ token = self ._get_client_token ()
91+ if token is None :
4092 return None
93+ return DatadogFeatureFlag (token , self .__app )
4194
95+
96+ @cached_property
97+ def __base_scopes (self ) -> dict [str , str ]:
98+ return self ._get_base_scopes ()
99+
100+ @cached_property
101+ def __entity (self ) -> str :
102+ return self ._get_entity ()
103+
104+
105+ class CIFeatureFlagManager (FeatureFlagManager ):
106+ """
107+ A class for querying feature flags in a CI environment.
108+ """
109+
110+ def _get_client_token (self ) -> str | None :
111+ self ._app .display_debug (f"Getting client token for { sys .platform } " )
112+ try :
113+ match sys .platform :
114+ case "win32" :
115+ return self .__get_client_token_windows ()
116+ case "darwin" :
117+ return self .__get_client_token_macos ()
118+ case "linux" :
119+ return self .__get_client_token_linux ()
120+ case _:
121+ return None
122+ except Exception as e : # noqa: BLE001
123+ self ._app .display_warning (f"Error getting client token: { e } , feature flag will be defaulted" )
124+ return None
125+
126+ def __get_client_token_windows (self ) -> str | None : # noqa: PLR6301
127+ if (client_token := os .getenv (AppEnvVars .FEATURE_FLAGS_CI_SSM_KEY_WINDOWS )) is None :
128+ return None
129+ return fetch_secret_ssm (name = client_token )
130+
131+ def __get_client_token_macos (self ) -> str | None : # noqa: PLR6301
132+ if (client_token := os .getenv (AppEnvVars .FEATURE_FLAGS_CI_VAULT_KEY_MACOS )) is None :
133+ return None
134+ if (vault_path := os .getenv (AppEnvVars .FEATURE_FLAGS_CI_VAULT_PATH_MACOS )) is None :
135+ return None
136+ return fetch_secret_ci (vault_path , client_token )
137+
138+ def __get_client_token_linux (self ) -> str | None : # noqa: PLR6301
139+ if (client_token := os .getenv (AppEnvVars .FEATURE_FLAGS_CI_VAULT_KEY )) is None :
140+ return None
141+ if (vault_path := os .getenv (AppEnvVars .FEATURE_FLAGS_CI_VAULT_PATH )) is None :
142+ return None
143+ return fetch_secret_ci (vault_path , client_token )
144+
145+ def _get_entity (self ) -> str : # noqa: PLR6301
146+ return os .getenv ("CI_JOB_ID" , "default_entity" )
147+
148+ def _get_base_scopes (self ) -> dict [str , str ]: # noqa: PLR6301
149+ return {
150+ "ci" : "true" ,
151+ "ci.job.name" : os .getenv ("CI_JOB_NAME" , "" ),
152+ "ci.job.id" : os .getenv ("CI_JOB_ID" , "" ),
153+ "ci.stage.name" : os .getenv ("CI_JOB_STAGE" , "" ),
154+ "git.branch" : os .getenv ("CI_COMMIT_BRANCH" , "" ),
155+ }
156+
157+
158+ class LocalFeatureFlagManager (FeatureFlagManager ):
159+ """
160+ A class for querying feature flags. This is available as the
161+ [`Application.features`][dda.cli.application.Application.features] property.
162+ """
163+
164+ def _get_client_token (self ) -> str | None : # noqa: PLR6301
42165 from contextlib import suppress
43166
44167 from dda .secrets .api import fetch_client_token , read_client_token , save_client_token
@@ -54,16 +177,12 @@ def __client(self) -> DatadogFeatureFlag | None:
54177
55178 @property
56179 def __user (self ) -> FeatureFlagUser :
57- return FeatureFlagUser (self .__app .config )
58-
59- def __get_entity (self ) -> str :
60- if running_in_ci ():
61- import os
62-
63- return os .getenv ("CI_JOB_ID" , "default_job_id" )
180+ return FeatureFlagUser (self ._app .config )
64181
182+ def _get_entity (self ) -> str :
65183 return self .__user .machine_id
66184
185+ < << << << HEAD
67186 def enabled (self , flag : str , * , default : bool = False , scopes : Optional [dict [str , str ]] = None ) -> bool :
68187 entity = self .__get_entity ()
69188 base_scopes = self .__get_base_scopes ()
@@ -93,7 +212,11 @@ def __check_flag(self, flag: str, entity: str, scopes: tuple[tuple[str, str], ..
93212 return flag_value
94213
95214 def __get_base_scopes (self ) -> dict [str , str ]:
215+ == == == =
216+ def _get_base_scopes (self ) -> dict [str , str ]:
217+ >> >> >> > be7a4ee (Add support for CI use for feature flags )
96218 return {
97- "ci" : "true" if running_in_ci () else "false" ,
219+ "platform" : get_os_name (),
220+ "ci" : "false" ,
98221 "user" : self .__user .email ,
99222 }
0 commit comments