1- import os .path
1+ import logging
2+ import os
23import time
34import typing
45from pathlib import Path
56
67import grpc
78from grpc_health .v1 import health_pb2 , health_pb2_grpc
8- from testcontainers .core .container import DockerContainer
9- from testcontainers .core .wait_strategies import LogMessageWaitStrategy
9+ from testcontainers .compose import DockerCompose
1010
1111from openfeature .contrib .provider .flagd .config import ResolverType
1212
13+ logger = logging .getLogger (__name__ )
14+
1315HEALTH_CHECK = 8014
1416LAUNCHPAD = 8080
1517FORBIDDEN = 9212
1618
1719
18- class FlagdContainer (DockerContainer ):
20+ class FlagdContainer :
21+ """Manages the docker-compose environment for flagd e2e tests.
22+
23+ Uses docker-compose to start both flagd and envoy containers,
24+ so the envoy forbidden endpoint (port 9212) returns a proper HTTP 403.
25+ """
26+
1927 def __init__ (
2028 self ,
2129 feature : typing .Optional [str ] = None ,
2230 ** kwargs ,
2331 ) -> None :
32+ self ._test_harness_dir = (
33+ Path (__file__ ).parents [2 ] / "openfeature" / "test-harness"
34+ )
35+ self ._version = (self ._test_harness_dir / "version.txt" ).read_text ().rstrip ()
36+
2437 image : str = "ghcr.io/open-feature/flagd-testbed"
2538 if feature is not None :
2639 image = f"{ image } -{ feature } "
27- path = Path (__file__ ).parents [2 ] / "openfeature/test-harness/version.txt"
28- data = path .read_text ().rstrip ()
29- super ().__init__ (f"{ image } :v{ data } " , ** kwargs )
30- self .rpc = 8013
31- self .ipr = 8015
40+
3241 self .flagDir = Path ("./flags" )
3342 self .flagDir .mkdir (parents = True , exist_ok = True )
34- self .with_exposed_ports (self .rpc , self .ipr , HEALTH_CHECK , LAUNCHPAD , FORBIDDEN )
35- self .with_volume_mapping (os .path .abspath (self .flagDir .name ), "/flags" , "rw" )
36- self .waiting_for (LogMessageWaitStrategy ("listening" ).with_startup_timeout (5 ))
3743
38- def get_port (self , resolver_type : ResolverType ):
44+ # Set environment variables for docker-compose substitution
45+ os .environ ["IMAGE" ] = image
46+ os .environ ["VERSION" ] = f"v{ self ._version } "
47+ os .environ ["FLAGS_DIR" ] = str (self .flagDir .absolute ())
48+
49+ self ._compose = DockerCompose (
50+ context = str (self ._test_harness_dir ),
51+ compose_file_name = "docker-compose.yaml" ,
52+ wait = True ,
53+ )
54+
55+ def get_port (self , resolver_type : ResolverType ) -> int :
3956 if resolver_type == ResolverType .RPC :
40- return self .get_exposed_port ( self . rpc )
57+ return self ._compose . get_service_port ( "flagd" , 8013 )
4158 else :
42- return self .get_exposed_port ( self . ipr )
59+ return self ._compose . get_service_port ( "flagd" , 8015 )
4360
44- def get_launchpad_url (self ):
45- return f"http://localhost:{ self .get_exposed_port (LAUNCHPAD )} "
61+ def get_exposed_port (self , port : int ) -> int :
62+ """Get mapped port. For FORBIDDEN (9212) returns envoy port, otherwise flagd port."""
63+ if port == FORBIDDEN :
64+ return self ._compose .get_service_port ("envoy" , FORBIDDEN )
65+ return self ._compose .get_service_port ("flagd" , port )
66+
67+ def get_launchpad_url (self ) -> str :
68+ port = self ._compose .get_service_port ("flagd" , LAUNCHPAD )
69+ return f"http://localhost:{ port } "
4670
4771 def start (self ) -> "FlagdContainer" :
48- super ().start ()
49- self ._checker (self .get_container_host_ip (), self .get_exposed_port (HEALTH_CHECK ))
72+ self ._compose .start ()
73+ host = self ._compose .get_service_host ("flagd" , HEALTH_CHECK ) or "localhost"
74+ port = self ._compose .get_service_port ("flagd" , HEALTH_CHECK )
75+ self ._checker (host , port )
5076 return self
5177
78+ def stop (self ) -> None :
79+ self ._compose .stop ()
80+
5281 def _checker (self , host : str , port : int ) -> None :
5382 # Give an extra second before continuing
5483 time .sleep (1 )
55- # Second we use the GRPC health check endpoint
56- with grpc .insecure_channel (host + ":" + str ( port ) ) as channel :
84+ # Use the GRPC health check endpoint
85+ with grpc .insecure_channel (f" { host } : { port } " ) as channel :
5786 health_stub = health_pb2_grpc .HealthStub (channel )
5887
5988 def health_check_call (stub : health_pb2_grpc .HealthStub ):
6089 request = health_pb2 .HealthCheckRequest ()
61- resp = stub . Check ( request )
62- if resp . status == health_pb2 . HealthCheckResponse . SERVING :
63- return True
64- elif resp . status == health_pb2 . HealthCheckResponse . NOT_SERVING :
90+ try :
91+ resp = stub . Check ( request )
92+ return resp . status == health_pb2 . HealthCheckResponse . SERVING
93+ except Exception :
6594 return False
6695
67- # Should succeed
6896 # Check health status every 1 second for 30 seconds
6997 ok = False
7098 for _ in range (30 ):
@@ -74,4 +102,4 @@ def health_check_call(stub: health_pb2_grpc.HealthStub):
74102 time .sleep (1 )
75103
76104 if not ok :
77- raise ConnectionError ("flagD not ready in time" )
105+ raise ConnectionError ("flagd not ready in time" )
0 commit comments