Skip to content

Commit 100c989

Browse files
marcozabeltoddbaert
authored andcommitted
feat: Make flagd defaultVariant optional
Signed-off-by: marcozabel <marco.zabel@dynatrace.com>
1 parent 80da6b9 commit 100c989

File tree

17 files changed

+364
-167
lines changed

17 files changed

+364
-167
lines changed

.gitmodules

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
[submodule "schemas"]
22
path = providers/openfeature-provider-flagd/openfeature/schemas
33
url = https://github.com/open-feature/schemas
4-
branch = protobuf-v0.6.1
54
[submodule "providers/openfeature-provider-flagd/spec"]
65
path = providers/openfeature-provider-flagd/openfeature/spec
76
url = https://github.com/open-feature/spec
87
[submodule "providers/openfeature-provider-flagd/openfeature/test-harness"]
98
path = providers/openfeature-provider-flagd/openfeature/test-harness
10-
url = https://github.com/open-feature/flagd-testbed.git
11-
branch = v2.11.1
9+
url = https://github.com/open-feature/flagd-testbed.git

providers/openfeature-provider-flagd/flags/.gitkeep

Whitespace-only changes.

providers/openfeature-provider-flagd/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ classifiers = [
1818
keywords = []
1919
dependencies = [
2020
"openfeature-sdk>=0.8.2",
21-
"grpcio>=1.76.0",
21+
"grpcio>=1.78.0",
2222
"protobuf>=6.30.0,<7.0.0",
2323
"mmh3>=5.0.0,<6.0.0",
2424
"panzi-json-logic>=1.0.1",

providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/config.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class CacheType(Enum):
2828
DEFAULT_PORT_RPC = 8013
2929
DEFAULT_RESOLVER_TYPE = ResolverType.RPC
3030
DEFAULT_RETRY_BACKOFF = 1000
31-
DEFAULT_RETRY_BACKOFF_MAX = 120000
31+
DEFAULT_RETRY_BACKOFF_MAX = 12000
3232
DEFAULT_RETRY_GRACE_PERIOD_SECONDS = 5
3333
DEFAULT_STREAM_DEADLINE = 600000
3434
DEFAULT_TLS = False
@@ -42,6 +42,8 @@ class CacheType(Enum):
4242
ENV_VAR_OFFLINE_FLAG_SOURCE_PATH = "FLAGD_OFFLINE_FLAG_SOURCE_PATH"
4343
ENV_VAR_OFFLINE_POLL_MS = "FLAGD_OFFLINE_POLL_MS"
4444
ENV_VAR_PORT = "FLAGD_PORT"
45+
ENV_VAR_SYNC_PORT = "FLAGD_SYNC_PORT"
46+
ENV_VAR_FATAL_STATUS_CODES = "FLAGD_FATAL_STATUS_CODES"
4547
ENV_VAR_RESOLVER_TYPE = "FLAGD_RESOLVER"
4648
ENV_VAR_RETRY_BACKOFF_MS = "FLAGD_RETRY_BACKOFF_MS"
4749
ENV_VAR_RETRY_BACKOFF_MAX_MS = "FLAGD_RETRY_BACKOFF_MAX_MS"
@@ -83,6 +85,7 @@ def __init__( # noqa: PLR0913
8385
self,
8486
host: typing.Optional[str] = None,
8587
port: typing.Optional[int] = None,
88+
sync_port: typing.Optional[int] = None,
8689
tls: typing.Optional[bool] = None,
8790
selector: typing.Optional[str] = None,
8891
provider_id: typing.Optional[str] = None,
@@ -101,6 +104,7 @@ def __init__( # noqa: PLR0913
101104
default_authority: typing.Optional[str] = None,
102105
channel_credentials: typing.Optional[grpc.ChannelCredentials] = None,
103106
sync_metadata_disabled: typing.Optional[bool] = None,
107+
fatal_status_codes: typing.Optional[list[str]] = None,
104108
):
105109
self.host = env_or_default(ENV_VAR_HOST, DEFAULT_HOST) if host is None else host
106110

@@ -110,6 +114,19 @@ def __init__( # noqa: PLR0913
110114
else tls
111115
)
112116

117+
self.fatal_status_codes: list[str] = (
118+
typing.cast(
119+
list[str],
120+
env_or_default(
121+
ENV_VAR_FATAL_STATUS_CODES,
122+
[],
123+
cast=lambda s: [item.strip() for item in s.split(",")],
124+
),
125+
)
126+
if fatal_status_codes is None
127+
else fatal_status_codes
128+
)
129+
113130
self.retry_backoff_ms: int = (
114131
int(
115132
env_or_default(
@@ -161,6 +178,14 @@ def __init__( # noqa: PLR0913
161178
else port
162179
)
163180

181+
self.port = (
182+
int(env_or_default(ENV_VAR_SYNC_PORT, self.port, cast=int))
183+
if sync_port is None and port is None
184+
else sync_port
185+
if sync_port is not None
186+
else self.port
187+
)
188+
164189
self.offline_flag_source_path = (
165190
env_or_default(
166191
ENV_VAR_OFFLINE_FLAG_SOURCE_PATH, DEFAULT_OFFLINE_SOURCE_PATH

providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/grpc.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
TypeMismatchError,
2323
)
2424
from openfeature.flag_evaluation import FlagResolutionDetails, FlagValueType, Reason
25-
from openfeature.schemas.protobuf.flagd.evaluation.v1 import (
25+
from openfeature.schemas.protobuf.flagd.evaluation.v2 import (
2626
evaluation_pb2,
2727
evaluation_pb2_grpc,
2828
)
@@ -88,7 +88,7 @@ def _generate_channel(self, config: Config) -> grpc.Channel:
8888
{
8989
"name": [
9090
{"service": "flagd.sync.v1.FlagSyncService"},
91-
{"service": "flagd.evaluation.v1.Service"},
91+
{"service": "flagd.evaluation.v2.Service"},
9292
],
9393
"retryPolicy": {
9494
"maxAttempts": 3,
@@ -237,7 +237,8 @@ def listen(self) -> None:
237237
)
238238
self.connected = True
239239
elif message.type == "configuration_change":
240-
data = MessageToDict(message)["data"]
240+
msg_dict = MessageToDict(message)
241+
data = msg_dict.get("data", {})
241242
self.handle_changed_flags(data)
242243

243244
if not self.active:
@@ -252,7 +253,7 @@ def listen(self) -> None:
252253
)
253254

254255
def handle_changed_flags(self, data: typing.Any) -> None:
255-
changed_flags = list(data["flags"].keys())
256+
changed_flags = list(data.get("flags", {}).keys())
256257

257258
if self.cache:
258259
for flag in changed_flags:
@@ -419,11 +420,16 @@ def _resolve( # noqa: PLR0915 C901
419420
raise ParseError(message) from e
420421
raise GeneralError(message) from e
421422

423+
# When no default variant is configured, the server returns an empty/zero proto
424+
# value with reason=DEFAULT. In that case, return the caller's code default value.
425+
if response.reason == Reason.DEFAULT and not response.variant:
426+
value = default_value
427+
422428
# Got a valid flag and valid type. Return it.
423429
result = FlagResolutionDetails(
424430
value=value,
425431
reason=response.reason,
426-
variant=response.variant,
432+
variant=response.variant or None,
427433
)
428434

429435
if response.reason == Reason.STATIC and self.cache is not None:

providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/in_process.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
)
66
from openfeature.evaluation_context import EvaluationContext
77
from openfeature.event import ProviderEventDetails
8-
from openfeature.exception import ErrorCode, FlagNotFoundError, GeneralError, ParseError
8+
from openfeature.exception import FlagNotFoundError, GeneralError, ParseError
99
from openfeature.flag_evaluation import FlagResolutionDetails, FlagValueType, Reason
1010

1111
from ..config import Config
@@ -132,12 +132,12 @@ def _resolve(
132132
)
133133

134134
if not flag.targeting:
135-
return _default_resolve(flag, metadata, Reason.STATIC)
135+
return _default_resolve(flag, metadata, Reason.STATIC, default_value)
136136

137137
try:
138138
variant = targeting(flag.key, flag.targeting, evaluation_context)
139139
if variant is None:
140-
return _default_resolve(flag, metadata, Reason.DEFAULT)
140+
return _default_resolve(flag, metadata, Reason.DEFAULT, default_value)
141141

142142
# convert to string to support shorthand (boolean in python is with capital T hence the special case)
143143
if isinstance(variant, bool):
@@ -169,14 +169,14 @@ def _default_resolve(
169169
flag: Flag,
170170
metadata: typing.Mapping[str, typing.Union[float, int, str, bool]],
171171
reason: Reason,
172+
default_value: typing.Any = None,
172173
) -> FlagResolutionDetails:
173174
variant, value = flag.default
174175
if variant is None:
175176
return FlagResolutionDetails(
176-
value,
177+
default_value,
177178
variant=variant,
178-
reason=Reason.ERROR,
179-
error_code=ErrorCode.FLAG_NOT_FOUND,
179+
reason=Reason.DEFAULT,
180180
flag_metadata=metadata,
181181
)
182182
if variant not in flag.variants:

providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def _generate_channel(self, config: Config) -> grpc.Channel:
6969
{
7070
"name": [
7171
{"service": "flagd.sync.v1.FlagSyncService"},
72-
{"service": "flagd.evaluation.v1.Service"},
72+
{"service": "flagd.evaluation.v2.Service"},
7373
],
7474
"retryPolicy": {
7575
"maxAttempts": 3,

providers/openfeature-provider-flagd/tests/e2e/file/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"~caching",
1414
"~grace",
1515
"~contextEnrichment",
16+
"~deprecated",
1617
}
1718

1819

0 commit comments

Comments
 (0)