Skip to content

Commit 7d5b35f

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

File tree

10 files changed

+215
-92
lines changed

10 files changed

+215
-92
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/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: 86 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import logging
22
import threading
3+
import typing
4+
from enum import Enum
5+
from pathlib import Path
36

47
import pytest
58
from pytest_bdd import given, parsers, when
6-
from testcontainers.core.container import DockerContainer
79
from tests.e2e.flagd_container import FlagdContainer
810
from tests.e2e.step._utils import wait_for
911

@@ -14,6 +16,14 @@
1416
from openfeature.provider import ProviderStatus
1517

1618

19+
class TestProviderType(Enum):
20+
UNAVAILABLE = "unavailable"
21+
STABLE = "stable"
22+
UNSTABLE = "unstable"
23+
SSL = "ssl"
24+
SOCKET = "socket"
25+
26+
1727
@given("a provider is registered", target_fixture="client")
1828
def setup_provider_old(
1929
container: FlagdContainer,
@@ -23,47 +33,83 @@ def setup_provider_old(
2333
setup_provider(container, resolver_type, "stable", dict)
2434

2535

26-
@given(parsers.cfparse("a {provider_type} flagd provider"), target_fixture="client")
36+
def get_default_options_for_provider(
37+
provider_type: str, resolver_type: ResolverType
38+
) -> typing.Tuple[dict, bool]:
39+
t = TestProviderType(provider_type)
40+
options: dict = {
41+
"resolver_type": resolver_type,
42+
"deadline_ms": 500,
43+
"stream_deadline_ms": 0,
44+
"retry_backoff_ms": 1000,
45+
"retry_grace_period": 2,
46+
}
47+
if t == TestProviderType.UNAVAILABLE:
48+
return {}, False
49+
elif t == TestProviderType.SSL:
50+
path = (
51+
Path(__file__).parents[3]
52+
/ "openfeature/test-harness/ssl/custom-root-cert.crt"
53+
)
54+
options["cert_path"] = str(path.absolute())
55+
options["tls"] = True
56+
elif t == TestProviderType.SOCKET:
57+
return options, True
58+
59+
return options, True
60+
61+
62+
@given(
63+
parsers.cfparse("a {provider_type} flagd provider"), target_fixture="provider_type"
64+
)
2765
def setup_provider(
28-
container: FlagdContainer,
66+
containers: dict,
2967
resolver_type: ResolverType,
3068
provider_type: str,
3169
option_values: dict,
3270
) -> OpenFeatureClient:
33-
if provider_type == "unavailable":
34-
api.set_provider(
35-
FlagdProvider(
36-
resolver_type=resolver_type,
37-
**option_values,
38-
),
39-
"unavailable",
71+
default_options, ready = get_default_options_for_provider(
72+
provider_type, resolver_type
73+
)
74+
75+
if ready:
76+
container = (
77+
containers.get(provider_type)
78+
if provider_type in containers
79+
else containers.get("default")
4080
)
41-
client = api.get_client("unavailable")
42-
return client
81+
try:
82+
container.get_port(resolver_type)
83+
except: # noqa: E722
84+
container.start()
4385

44-
try:
45-
container.get_port(resolver_type)
46-
except: # noqa: E722
47-
container.start()
86+
default_options["port"] = container.get_port(resolver_type)
87+
88+
combined_options = {**default_options, **option_values}
4889
api.set_provider(
49-
FlagdProvider(
50-
resolver_type=resolver_type,
51-
port=container.get_port(resolver_type),
52-
deadline_ms=500,
53-
stream_deadline_ms=0,
54-
retry_backoff_ms=1000,
55-
**option_values,
56-
),
90+
FlagdProvider(**combined_options),
5791
provider_type,
5892
)
5993
client = api.get_client(provider_type)
6094

61-
wait_for(lambda: client.get_provider_status() == ProviderStatus.READY)
62-
return client
95+
wait_for(
96+
lambda: client.get_provider_status() == ProviderStatus.READY
97+
) if ready else None
98+
return provider_type
99+
100+
101+
@pytest.fixture()
102+
def client(ptype: str) -> OpenFeatureClient:
103+
return api.get_client(ptype)
63104

64105

65106
@when(parsers.cfparse("the connection is lost for {seconds}s"))
66-
def flagd_restart(seconds, container: FlagdContainer):
107+
def flagd_restart(seconds, containers: dict, provider_type: str):
108+
container = (
109+
containers.get(provider_type)
110+
if provider_type in containers
111+
else containers.get("default")
112+
)
67113
ipr_port = container.get_port(ResolverType.IN_PROCESS)
68114
rpc_port = container.get_port(ResolverType.RPC)
69115

@@ -78,19 +124,23 @@ def starting():
78124

79125

80126
@pytest.fixture(autouse=True, scope="module")
81-
def container(request):
82-
container: DockerContainer = FlagdContainer()
127+
def containers(request):
128+
containers = {
129+
"default": FlagdContainer(),
130+
"ssl": FlagdContainer("ssl"),
131+
}
83132

84-
# Setup code
85-
container.start()
133+
[containers[c].start() for c in containers]
86134

87135
def fin():
88-
try:
89-
container.stop()
90-
except: # noqa: E722
91-
logging.debug("container was not running anymore")
136+
for name in containers.items():
137+
container = containers[name]
138+
try:
139+
container.stop()
140+
except: # noqa: E722
141+
logging.debug(f"container '{name}' was not running anymore")
92142

93143
# Teardown code
94144
request.addfinalizer(fin)
95145

96-
return container
146+
return containers

0 commit comments

Comments
 (0)