Skip to content

Commit f50351a

Browse files
authored
feat(flagd): add custom cert path (#131)
feat(flagd): add ssl cert path option Signed-off-by: Simon Schrottner <[email protected]>
1 parent f6431e6 commit f50351a

File tree

5 files changed

+117
-22
lines changed

5 files changed

+117
-22
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_type,
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: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,25 +64,41 @@ 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
6872

73+
self.start_time = time.time()
74+
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
]
76-
self.channel = channel_factory(
77-
f"{config.host}:{config.port}",
78-
options=options,
79-
)
80-
self.stub = evaluation_pb2_grpc.ServiceStub(self.channel)
81-
82-
self.thread: typing.Optional[threading.Thread] = None
83-
self.timer: typing.Optional[threading.Timer] = None
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+
)
84100

85-
self.start_time = time.time()
101+
return channel
86102

87103
def initialize(self, evaluation_context: EvaluationContext) -> None:
88104
self.connect()
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from pathlib import Path
2+
3+
import pytest
4+
from pytest_bdd import given, scenarios
5+
from tests.e2e.conftest import SPEC_PATH
6+
from tests.e2e.flagd_container import FlagdContainer
7+
from tests.e2e.steps import wait_for
8+
9+
from openfeature import api
10+
from openfeature.client import OpenFeatureClient
11+
from openfeature.contrib.provider.flagd import FlagdProvider
12+
from openfeature.contrib.provider.flagd.config import ResolverType
13+
from openfeature.provider import ProviderStatus
14+
15+
16+
@pytest.fixture(autouse=True, scope="module")
17+
def client_name() -> str:
18+
return "rpc"
19+
20+
21+
@pytest.fixture(autouse=True, scope="module")
22+
def resolver_type() -> ResolverType:
23+
return ResolverType.RPC
24+
25+
26+
@pytest.fixture(autouse=True, scope="module")
27+
def port():
28+
return 8013
29+
30+
31+
@pytest.fixture(autouse=True, scope="module")
32+
def image():
33+
return "ghcr.io/open-feature/flagd-testbed-ssl"
34+
35+
36+
@given("a flagd provider is set", target_fixture="client")
37+
@given("a provider is registered", target_fixture="client")
38+
def setup_provider(
39+
container: FlagdContainer, resolver_type, client_name, port
40+
) -> OpenFeatureClient:
41+
try:
42+
container.get_exposed_port(port)
43+
except: # noqa: E722
44+
container.start()
45+
46+
path = (
47+
Path(__file__).parents[2] / "openfeature/test-harness/ssl/custom-root-cert.crt"
48+
)
49+
50+
api.set_provider(
51+
FlagdProvider(
52+
resolver_type=resolver_type,
53+
port=int(container.get_exposed_port(port)),
54+
timeout=1,
55+
retry_grace_period=3,
56+
tls=True,
57+
cert_path=str(path.absolute()),
58+
),
59+
client_name,
60+
)
61+
client = api.get_client(client_name)
62+
wait_for(lambda: client.get_provider_status() == ProviderStatus.READY)
63+
return client
64+
65+
66+
scenarios(
67+
f"{SPEC_PATH}/specification/assets/gherkin/evaluation.feature",
68+
)

0 commit comments

Comments
 (0)