Skip to content

Commit

Permalink
feat(flagd): add ssl cert path option
Browse files Browse the repository at this point in the history
Signed-off-by: Simon Schrottner <[email protected]>
  • Loading branch information
aepfli committed Dec 27, 2024
1 parent 8e23a70 commit e07ce8d
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 22 deletions.
24 changes: 12 additions & 12 deletions providers/openfeature-provider-flagd/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,12 @@ api.set_provider(FlagdProvider(
The default options can be defined in the FlagdProvider constructor.

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

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

<!--

You may optionally supply an X.509 certificate in PEM format. Otherwise, the default certificate store will be used.
```java
FlagdProvider flagdProvider = new FlagdProvider(
FlagdOptions.builder()
.host("myflagdhost")
.tls(true) // use TLS
.certPath("etc/cert/ca.crt") // PEM cert
.build());

```python
from openfeature import api
from openfeature.contrib.provider.flagd import FlagdProvider

api.set_provider(FlagdProvider(
tls=True, # use TLS
cert_path="etc/cert/ca.crt" # PEM cert
))
```
-->

## License

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class CacheType(Enum):
DEFAULT_RETRY_GRACE_PERIOD_SECONDS = 5
DEFAULT_STREAM_DEADLINE = 600000
DEFAULT_TLS = False
DEFAULT_TLS_CERT: typing.Optional[str] = None

ENV_VAR_CACHE_SIZE = "FLAGD_MAX_CACHE_SIZE"
ENV_VAR_CACHE_TYPE = "FLAGD_CACHE"
Expand All @@ -44,6 +45,7 @@ class CacheType(Enum):
ENV_VAR_RETRY_GRACE_PERIOD_SECONDS = "FLAGD_RETRY_GRACE_PERIOD"
ENV_VAR_STREAM_DEADLINE_MS = "FLAGD_STREAM_DEADLINE_MS"
ENV_VAR_TLS = "FLAGD_TLS"
ENV_VAR_TLS_CERT = "FLAGD_SERVER_CERT_PATH"

T = typing.TypeVar("T")

Expand Down Expand Up @@ -87,6 +89,7 @@ def __init__( # noqa: PLR0913
keep_alive_time: typing.Optional[int] = None,
cache: typing.Optional[CacheType] = None,
max_cache_size: typing.Optional[int] = None,
cert_path: typing.Optional[str] = None,
):
self.host = env_or_default(ENV_VAR_HOST, DEFAULT_HOST) if host is None else host

Expand Down Expand Up @@ -200,3 +203,9 @@ def __init__( # noqa: PLR0913
if max_cache_size is None
else max_cache_size
)

self.cert_path = (
env_or_default(ENV_VAR_TLS_CERT, DEFAULT_TLS_CERT)
if cert_path is None
else cert_path
)
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ def __init__( # noqa: PLR0913
max_cache_size: typing.Optional[int] = None,
retry_backoff_max_ms: typing.Optional[int] = None,
retry_grace_period: typing.Optional[int] = None,
cert_path: typing.Optional[str] = None,
):
"""
Create an instance of the FlagdProvider
Expand Down Expand Up @@ -91,6 +92,7 @@ def __init__( # noqa: PLR0913
keep_alive_time=keep_alive_time,
cache=cache_type,
max_cache_size=max_cache_size,
cert_path=cert_path,
)

self.resolver = self.setup_resolver()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,25 +64,41 @@ def __init__(
self.streamline_deadline_seconds = config.stream_deadline_ms * 0.001
self.deadline = config.deadline_ms * 0.001
self.connected = False
channel_factory = grpc.secure_channel if config.tls else grpc.insecure_channel
self.channel = self._generate_channel(config)
self.stub = evaluation_pb2_grpc.ServiceStub(self.channel)

self.thread: typing.Optional[threading.Thread] = None
self.timer: typing.Optional[threading.Timer] = None

self.start_time = time.time()

def _generate_channel(self, config: Config) -> grpc.Channel:
target = f"{config.host}:{config.port}"
# Create the channel with the service config
options = [
("grpc.keepalive_time_ms", config.keep_alive_time),
("grpc.initial_reconnect_backoff_ms", config.retry_backoff_ms),
("grpc.max_reconnect_backoff_ms", config.retry_backoff_max_ms),
("grpc.min_reconnect_backoff_ms", config.deadline_ms),
]
self.channel = channel_factory(
f"{config.host}:{config.port}",
options=options,
)
self.stub = evaluation_pb2_grpc.ServiceStub(self.channel)

self.thread: typing.Optional[threading.Thread] = None
self.timer: typing.Optional[threading.Timer] = None
if config.tls:
channel_args = {
"options": options,
"credentials": grpc.ssl_channel_credentials(),
}
if config.cert_path:
with open(config.cert_path, "rb") as f:
channel_args["credentials"] = grpc.ssl_channel_credentials(f.read())

channel = grpc.secure_channel(target, **channel_args)

else:
channel = grpc.insecure_channel(
target,
options=options,
)

self.start_time = time.time()
return channel

def initialize(self, evaluation_context: EvaluationContext) -> None:
self.connect()
Expand Down
68 changes: 68 additions & 0 deletions providers/openfeature-provider-flagd/tests/e2e/test_rpc_ssl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from pathlib import Path

import pytest
from pytest_bdd import given, scenarios
from tests.e2e.conftest import SPEC_PATH
from tests.e2e.flagd_container import FlagdContainer
from tests.e2e.steps import wait_for

from openfeature import api
from openfeature.client import OpenFeatureClient
from openfeature.contrib.provider.flagd import FlagdProvider
from openfeature.contrib.provider.flagd.config import ResolverType
from openfeature.provider import ProviderStatus


@pytest.fixture(autouse=True, scope="module")
def client_name() -> str:
return "rpc"


@pytest.fixture(autouse=True, scope="module")
def resolver_type() -> ResolverType:
return ResolverType.RPC


@pytest.fixture(autouse=True, scope="module")
def port():
return 8013


@pytest.fixture(autouse=True, scope="module")
def image():
return "ghcr.io/open-feature/flagd-testbed-ssl"


@given("a flagd provider is set", target_fixture="client")
@given("a provider is registered", target_fixture="client")
def setup_provider(
container: FlagdContainer, resolver_type, client_name, port
) -> OpenFeatureClient:
try:
container.get_exposed_port(port)
except: # noqa: E722
container.start()

path = (
Path(__file__).parents[2] / "openfeature/test-harness/ssl/custom-root-cert.crt"
)

api.set_provider(
FlagdProvider(
resolver_type=resolver_type,
port=int(container.get_exposed_port(port)),
timeout=1,
retry_grace_period=3,
tls=True,
cert_path=str(path.absolute()),
),
client_name,
)
client = api.get_client(client_name)
wait_for(lambda: client.get_provider_status() == ProviderStatus.READY)
return client


scenarios(
f"{SPEC_PATH}/specification/assets/gherkin/evaluation.feature",
)

0 comments on commit e07ce8d

Please sign in to comment.