Skip to content

Commit 9192c77

Browse files
committed
Merge remote-tracking branch 'upstream/main' into feat/gherkinmigration
Signed-off-by: Simon Schrottner <[email protected]>
2 parents 67a6850 + f50351a commit 9192c77

File tree

12 files changed

+170
-101
lines changed

12 files changed

+170
-101
lines changed

providers/openfeature-provider-flagd/README.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,12 @@ api.set_provider(FlagdProvider(
4747
The default options can be defined in the FlagdProvider constructor.
4848

4949
| Option name | Environment variable name | Type & Values | Default | Compatible resolver |
50-
| ------------------------ | ------------------------------ | -------------------------- | ----------------------------- | ------------------- |
50+
|--------------------------|--------------------------------|----------------------------|-------------------------------|---------------------|
5151
| resolver_type | FLAGD_RESOLVER | enum - `rpc`, `in-process` | rpc | |
5252
| host | FLAGD_HOST | str | localhost | rpc & in-process |
5353
| port | FLAGD_PORT | int | 8013 (rpc), 8015 (in-process) | rpc & in-process |
5454
| tls | FLAGD_TLS | bool | false | rpc & in-process |
55+
| cert_path | FLAGD_SERVER_CERT_PATH | String | null | rpc & in-process |
5556
| deadline | FLAGD_DEADLINE_MS | int | 500 | rpc & in-process |
5657
| stream_deadline_ms | FLAGD_STREAM_DEADLINE_MS | int | 600000 | rpc & in-process |
5758
| keep_alive_time | FLAGD_KEEP_ALIVE_TIME_MS | int | 0 | rpc & in-process |
@@ -64,8 +65,6 @@ The default options can be defined in the FlagdProvider constructor.
6465
<!-- not implemented
6566
| target_uri | FLAGD_TARGET_URI | alternative to host/port, supporting custom name resolution | string | null | rpc & in-process |
6667
| socket_path | FLAGD_SOCKET_PATH | alternative to host port, unix socket | String | null | rpc & in-process |
67-
| cert_path | FLAGD_SERVER_CERT_PATH | tls cert path | String | null | rpc & in-process |
68-
| max_event_stream_retries | FLAGD_MAX_EVENT_STREAM_RETRIES | int | 5 | rpc |
6968
| context_enricher | - | sync-metadata to evaluation context mapping function | function | identity function | in-process |
7069
| offline_pollIntervalMs | FLAGD_OFFLINE_POLL_MS | poll interval for reading offlineFlagSourcePath | int | 5000 | in-process |
7170
-->
@@ -100,17 +99,18 @@ and the evaluation will default.
10099

101100
TLS is available in situations where flagd is running on another host.
102101

103-
<!--
102+
104103
You may optionally supply an X.509 certificate in PEM format. Otherwise, the default certificate store will be used.
105-
```java
106-
FlagdProvider flagdProvider = new FlagdProvider(
107-
FlagdOptions.builder()
108-
.host("myflagdhost")
109-
.tls(true) // use TLS
110-
.certPath("etc/cert/ca.crt") // PEM cert
111-
.build());
104+
105+
```python
106+
from openfeature import api
107+
from openfeature.contrib.provider.flagd import FlagdProvider
108+
109+
api.set_provider(FlagdProvider(
110+
tls=True, # use TLS
111+
cert_path="etc/cert/ca.crt" # PEM cert
112+
))
112113
```
113-
-->
114114

115115
## License
116116

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class CacheType(Enum):
2929
DEFAULT_RETRY_GRACE_PERIOD_SECONDS = 5
3030
DEFAULT_STREAM_DEADLINE = 600000
3131
DEFAULT_TLS = False
32+
DEFAULT_TLS_CERT: typing.Optional[str] = None
3233

3334
ENV_VAR_CACHE_SIZE = "FLAGD_MAX_CACHE_SIZE"
3435
ENV_VAR_CACHE_TYPE = "FLAGD_CACHE"
@@ -44,6 +45,7 @@ class CacheType(Enum):
4445
ENV_VAR_RETRY_GRACE_PERIOD_SECONDS = "FLAGD_RETRY_GRACE_PERIOD"
4546
ENV_VAR_STREAM_DEADLINE_MS = "FLAGD_STREAM_DEADLINE_MS"
4647
ENV_VAR_TLS = "FLAGD_TLS"
48+
ENV_VAR_TLS_CERT = "FLAGD_SERVER_CERT_PATH"
4749

4850
T = typing.TypeVar("T")
4951

@@ -87,6 +89,7 @@ def __init__( # noqa: PLR0913
8789
keep_alive_time: typing.Optional[int] = None,
8890
cache: typing.Optional[CacheType] = None,
8991
max_cache_size: typing.Optional[int] = None,
92+
cert_path: typing.Optional[str] = None,
9093
):
9194
self.host = env_or_default(ENV_VAR_HOST, DEFAULT_HOST) if host is None else host
9295

@@ -200,3 +203,9 @@ def __init__( # noqa: PLR0913
200203
if max_cache_size is None
201204
else max_cache_size
202205
)
206+
207+
self.cert_path = (
208+
env_or_default(ENV_VAR_TLS_CERT, DEFAULT_TLS_CERT)
209+
if cert_path is None
210+
else cert_path
211+
)

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ def __init__( # noqa: PLR0913
5454
max_cache_size: typing.Optional[int] = None,
5555
retry_backoff_max_ms: typing.Optional[int] = None,
5656
retry_grace_period: typing.Optional[int] = None,
57+
cert_path: typing.Optional[str] = None,
5758
):
5859
"""
5960
Create an instance of the FlagdProvider
@@ -91,6 +92,7 @@ def __init__( # noqa: PLR0913
9192
keep_alive_time=keep_alive_time,
9293
cache=cache,
9394
max_cache_size=max_cache_size,
95+
cert_path=cert_path,
9496
)
9597

9698
self.resolver = self.setup_resolver()

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

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -64,37 +64,50 @@ def __init__(
6464
self.streamline_deadline_seconds = config.stream_deadline_ms * 0.001
6565
self.deadline = config.deadline_ms * 0.001
6666
self.connected = False
67-
channel_factory = grpc.secure_channel if config.tls else grpc.insecure_channel
67+
self.channel = self._generate_channel(config)
68+
self.stub = evaluation_pb2_grpc.ServiceStub(self.channel)
69+
70+
self.thread: typing.Optional[threading.Thread] = None
71+
self.timer: typing.Optional[threading.Timer] = None
72+
73+
self.start_time = time.time()
6874

75+
def _generate_channel(self, config: Config) -> grpc.Channel:
76+
target = f"{config.host}:{config.port}"
6977
# Create the channel with the service config
7078
options = [
7179
("grpc.keepalive_time_ms", config.keep_alive_time),
7280
("grpc.initial_reconnect_backoff_ms", config.retry_backoff_ms),
7381
("grpc.max_reconnect_backoff_ms", config.retry_backoff_max_ms),
7482
("grpc.min_reconnect_backoff_ms", config.deadline_ms),
7583
]
84+
if config.tls:
85+
channel_args = {
86+
"options": options,
87+
"credentials": grpc.ssl_channel_credentials(),
88+
}
89+
if config.cert_path:
90+
with open(config.cert_path, "rb") as f:
91+
channel_args["credentials"] = grpc.ssl_channel_credentials(f.read())
92+
93+
channel = grpc.secure_channel(target, **channel_args)
94+
95+
else:
96+
channel = grpc.insecure_channel(
97+
target,
98+
options=options,
99+
)
76100

77-
self.channel = channel_factory(
78-
f"{config.host}:{config.port}",
79-
options=options,
80-
)
81-
self.stub = evaluation_pb2_grpc.ServiceStub(self.channel)
82-
83-
self.thread: typing.Optional[threading.Thread] = None
84-
self.timer: typing.Optional[threading.Timer] = None
85-
self.active = False
86-
87-
self.thread: typing.Optional[threading.Thread] = None
88-
self.timer: typing.Optional[threading.Timer] = None
89-
90-
self.start_time = time.time()
101+
return channel
91102

92103
def initialize(self, evaluation_context: EvaluationContext) -> None:
93104
self.connect()
94105

95106
def shutdown(self) -> None:
96107
self.active = False
97108
self.channel.close()
109+
if self.cache:
110+
self.cache.clear()
98111

99112
def connect(self) -> None:
100113
self.active = True
@@ -166,16 +179,13 @@ def listen(self) -> None:
166179
if self.streamline_deadline_seconds > 0
167180
else {}
168181
)
169-
call_args["wait_for_ready"] = True
170182
request = evaluation_pb2.EventStreamRequest()
171183

172184
# defining a never ending loop to recreate the stream
173185
while self.active:
174186
try:
175-
logger.info("Setting up gRPC sync flags connection")
176-
for message in self.stub.EventStream(
177-
request, wait_for_ready=True, **call_args
178-
):
187+
logger.debug("Setting up gRPC sync flags connection")
188+
for message in self.stub.EventStream(request, **call_args):
179189
if message.type == "provider_ready":
180190
self.connected = True
181191
self.emit_provider_ready(

providers/openfeature-provider-flagd/tests/e2e/flagd_container.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import time
2+
import typing
23
from pathlib import Path
34

45
import grpc
@@ -14,9 +15,12 @@
1415
class FlagdContainer(DockerContainer):
1516
def __init__(
1617
self,
18+
feature: typing.Optional[str] = None,
1719
**kwargs,
1820
) -> None:
1921
image: str = "ghcr.io/open-feature/flagd-testbed"
22+
if feature is not None:
23+
image = f"{image}-{feature}"
2024
path = Path(__file__).parents[2] / "openfeature/test-harness/version.txt"
2125
data = path.read_text().rstrip()
2226
super().__init__(f"{image}:v{data}", **kwargs)

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,12 @@ def all_flags(request):
7575
@pytest.fixture(params=["json", "yaml"], scope="module")
7676
def file_name(request, all_flags):
7777
extension = request.param
78-
outfile = tempfile.NamedTemporaryFile("w", delete=False, suffix="." + extension)
79-
write_test_file(outfile, all_flags)
80-
yield outfile
81-
return outfile
78+
with tempfile.NamedTemporaryFile(
79+
"w", delete=False, suffix="." + extension
80+
) as outfile:
81+
write_test_file(outfile, all_flags)
82+
yield outfile
83+
return outfile
8284

8385

8486
def write_test_file(outfile, all_flags):
@@ -110,7 +112,7 @@ def changed_flag(
110112

111113

112114
@pytest.fixture(autouse=True)
113-
def container(request, file_name, all_flags, option_values):
115+
def containers(request, file_name, all_flags, option_values):
114116
api.set_provider(
115117
FlagdProvider(
116118
resolver_type=ResolverType.IN_PROCESS,
Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
from e2e.conftest import SPEC_PATH
1+
import sys
2+
23
from pytest_bdd import scenarios
3-
from tests.e2e.conftest import TEST_HARNESS_PATH
4+
from tests.e2e.conftest import SPEC_PATH, TEST_HARNESS_PATH
5+
6+
# as soon as we support all the features, we can actually remove this limitation to not run on Python 3.8
7+
# Python 3.8 does not fully support tagging, hence that it will run all cases
8+
if sys.version_info >= (3, 9):
9+
scenarios(f"{TEST_HARNESS_PATH}/gherkin")
410

5-
scenarios(f"{TEST_HARNESS_PATH}/gherkin", f"{SPEC_PATH}/specification/assets/gherkin")
11+
scenarios(f"{SPEC_PATH}/specification/assets/gherkin")

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
# from tests.e2e.step.provider_steps import *
99

1010
resolver = ResolverType.RPC
11-
feature_list = ["~targetURI", "~customCert", "~unixsocket", "~sync"]
11+
feature_list = ["~targetURI", "~unixsocket", "~sync"]
1212

1313

1414
def pytest_collection_modifyitems(config, items):
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1+
import sys
2+
13
from pytest_bdd import scenarios
24
from tests.e2e.conftest import SPEC_PATH, TEST_HARNESS_PATH
35

4-
scenarios(f"{TEST_HARNESS_PATH}/gherkin", f"{SPEC_PATH}/specification/assets/gherkin")
6+
# as soon as we support all the features, we can actually remove this limitation to not run on Python 3.8
7+
# Python 3.8 does not fully support tagging, hence that it will run all cases
8+
if sys.version_info >= (3, 9):
9+
scenarios(f"{TEST_HARNESS_PATH}/gherkin")
10+
11+
scenarios(f"{SPEC_PATH}/specification/assets/gherkin")

0 commit comments

Comments
 (0)