From f6431e6a956f9165861a84f692a6a4b61b248ddf Mon Sep 17 00:00:00 2001 From: Simon Schrottner Date: Fri, 27 Dec 2024 14:11:14 +0100 Subject: [PATCH 1/2] build(renovate): Utilize default OpenFeature Renovate configuration (#132) We do have a default OpenFeature Renovate configuration within our community-tooling repository. (https://github.com/open-feature/community-tooling/blob/main/renovate.json) To reduce maintenance efforts, we should stick to the general one as a basis. Signed-off-by: Simon Schrottner --- renovate.json | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/renovate.json b/renovate.json index 6c50112..28b2e68 100644 --- a/renovate.json +++ b/renovate.json @@ -1,30 +1,10 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": [ - "config:recommended" - ], - "semanticCommits": "enabled", + "extends": ["github>open-feature/community-tooling"], "pep621": { "enabled": true }, "pre-commit": { "enabled": true - }, - "packageRules": [ - { - "description": "Automerge non-major updates", - "matchUpdateTypes": [ - "minor", - "patch" - ], - "matchCurrentVersion": "!/^0/", - "automerge": true - }, - { - "matchManagers": [ - "github-actions" - ], - "automerge": true - } - ] + } } From f50351a0435064111fb98753a49139fafa8307e6 Mon Sep 17 00:00:00 2001 From: Simon Schrottner Date: Fri, 27 Dec 2024 14:48:04 +0100 Subject: [PATCH 2/2] feat(flagd): add custom cert path (#131) feat(flagd): add ssl cert path option Signed-off-by: Simon Schrottner --- .../openfeature-provider-flagd/README.md | 24 +++---- .../contrib/provider/flagd/config.py | 9 +++ .../contrib/provider/flagd/provider.py | 2 + .../contrib/provider/flagd/resolvers/grpc.py | 36 +++++++--- .../tests/e2e/test_rpc_ssl.py | 68 +++++++++++++++++++ 5 files changed, 117 insertions(+), 22 deletions(-) create mode 100644 providers/openfeature-provider-flagd/tests/e2e/test_rpc_ssl.py diff --git a/providers/openfeature-provider-flagd/README.md b/providers/openfeature-provider-flagd/README.md index 6833da0..8463700 100644 --- a/providers/openfeature-provider-flagd/README.md +++ b/providers/openfeature-provider-flagd/README.md @@ -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 | @@ -64,8 +65,6 @@ The default options can be defined in the FlagdProvider constructor. @@ -100,17 +99,18 @@ and the evaluation will default. TLS is available in situations where flagd is running on another host. - ## License diff --git a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/config.py b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/config.py index 1eb3746..68efb57 100644 --- a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/config.py +++ b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/config.py @@ -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" @@ -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") @@ -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 @@ -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 + ) diff --git a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/provider.py b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/provider.py index dd8beea..588e9c2 100644 --- a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/provider.py +++ b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/provider.py @@ -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 @@ -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() diff --git a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/grpc.py b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/grpc.py index 5841912..cb34577 100644 --- a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/grpc.py +++ b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/grpc.py @@ -64,8 +64,16 @@ 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), @@ -73,16 +81,24 @@ def __init__( ("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() diff --git a/providers/openfeature-provider-flagd/tests/e2e/test_rpc_ssl.py b/providers/openfeature-provider-flagd/tests/e2e/test_rpc_ssl.py new file mode 100644 index 0000000..3a3214b --- /dev/null +++ b/providers/openfeature-provider-flagd/tests/e2e/test_rpc_ssl.py @@ -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", +)