diff --git a/interfaces/otlp/CHANGELOG.md b/interfaces/otlp/CHANGELOG.md index 8fa1859f..f4a72530 100644 --- a/interfaces/otlp/CHANGELOG.md +++ b/interfaces/otlp/CHANGELOG.md @@ -27,3 +27,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Replace the requirer's rule path interface with an interface accepting an object containing rules - Generic PromQL alert rules for requirer charms that are of the aggregator or application type + +## [0.3.0] + +### Added + +- Added an `insecure` field to the OtlpEndpoint(s) in the databag to communicate TLS, required for some gRPC applications diff --git a/interfaces/otlp/src/charmlibs/interfaces/otlp/_otlp.py b/interfaces/otlp/src/charmlibs/interfaces/otlp/_otlp.py index 1d89e532..85a70262 100644 --- a/interfaces/otlp/src/charmlibs/interfaces/otlp/_otlp.py +++ b/interfaces/otlp/src/charmlibs/interfaces/otlp/_otlp.py @@ -156,6 +156,10 @@ class OtlpEndpoint(BaseModel): telemetries: Sequence[str] = Field( description='Telemetry signal types accepted by this endpoint.' ) + insecure: bool = Field( + description='Whether this endpoint requires an insecure connection (e.g. no TLS).', + default=False, + ) class _OtlpEndpoint(OtlpEndpoint): @@ -431,13 +435,16 @@ def add_endpoint( protocol: Literal['http', 'grpc'], endpoint: str, telemetries: Sequence[Literal['logs', 'metrics', 'traces']], + insecure: bool = False, ) -> 'OtlpProvider': - """Add an OtlpEndpoint to the list of endpoints to publish. - - Call this method after endpoint-changing events e.g. TLS and ingress. - """ + """Add an OtlpEndpoint to the list of endpoints to publish.""" self._endpoints.append( - _OtlpEndpoint(protocol=protocol, endpoint=endpoint, telemetries=telemetries) + _OtlpEndpoint( + protocol=protocol, + endpoint=endpoint, + telemetries=telemetries, + insecure=insecure, + ) ) return self diff --git a/interfaces/otlp/src/charmlibs/interfaces/otlp/_version.py b/interfaces/otlp/src/charmlibs/interfaces/otlp/_version.py index a5062d2d..b7e53041 100644 --- a/interfaces/otlp/src/charmlibs/interfaces/otlp/_version.py +++ b/interfaces/otlp/src/charmlibs/interfaces/otlp/_version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = '0.2.0' +__version__ = '0.3.0' diff --git a/interfaces/otlp/tests/unit/test_endpoints.py b/interfaces/otlp/tests/unit/test_endpoints.py index 95711a20..c2f03d63 100644 --- a/interfaces/otlp/tests/unit/test_endpoints.py +++ b/interfaces/otlp/tests/unit/test_endpoints.py @@ -5,7 +5,7 @@ import json from collections.abc import Sequence -from typing import Any, Final, Literal, cast +from typing import Any, Final, Literal import ops import pytest @@ -14,7 +14,12 @@ from charmlibs.interfaces.otlp._otlp import DEFAULT_PROVIDER_RELATION_NAME as RECEIVE from charmlibs.interfaces.otlp._otlp import DEFAULT_REQUIRER_RELATION_NAME as SEND -from charmlibs.interfaces.otlp._otlp import OtlpRequirer, _OtlpEndpoint, _OtlpProviderAppData +from charmlibs.interfaces.otlp._otlp import ( + OtlpProvider, + OtlpRequirer, + _OtlpEndpoint, + _OtlpProviderAppData, +) from conftest import ALL_PROTOCOLS, ALL_TELEMETRIES PROTOCOLS: Final[list[Literal['http', 'grpc']]] = ['http', 'grpc'] @@ -28,6 +33,7 @@ def test_new_endpoint_key_is_ignored_by_databag_model(): 'protocol': 'new_protocol', 'endpoint': 'http://host:4317', 'telemetries': ['logs'], + 'insecure': False, 'new_key': 'value', } @@ -53,11 +59,13 @@ def test_new_endpoint_key_is_ignored_by_databag_model(): 'protocol': 'new_protocol', 'endpoint': 'http://host:0000', 'telemetries': ['metrics'], + 'insecure': False, }, { 'protocol': 'http', 'endpoint': 'http://host:4317', 'telemetries': ['metrics'], + 'insecure': False, }, ]), }, @@ -75,13 +83,12 @@ def test_new_endpoint_key_is_ignored_by_databag_model(): 'protocol': 'http', 'endpoint': 'http://host:4317', 'telemetries': ['logs', 'new_telemetry', 'traces'], + 'insecure': False, }, ]), }, _OtlpEndpoint( - protocol='http', - endpoint='http://host:4317', - telemetries=['logs', 'traces'], + protocol='http', endpoint='http://host:4317', telemetries=['logs', 'traces'] ), ), ( @@ -93,6 +100,7 @@ def test_new_endpoint_key_is_ignored_by_databag_model(): 'protocol': 'http', 'endpoint': 'http://host:4317', 'telemetries': ['metrics'], + 'insecure': False, } ]), 'does_not': '"exist"', @@ -118,11 +126,10 @@ def test_send_otlp_invalid_databag( with otlp_requirer_ctx(otlp_requirer_ctx.on.update_status(), state=state) as mgr: # WHEN the requirer processes the relation data # * the requirer supports all protocols and telemetries - charm_any = cast('Any', mgr.charm) # THEN the requirer does not raise an error # * the returned endpoint does not include new protocols or telemetries assert mgr.run() - endpoints = OtlpRequirer(charm_any, SEND, ALL_PROTOCOLS, ALL_TELEMETRIES).endpoints + endpoints = OtlpRequirer(mgr.charm, SEND, ALL_PROTOCOLS, ALL_TELEMETRIES).endpoints assert endpoints[123].model_dump() == otlp_endpoint.model_dump() @@ -135,13 +142,11 @@ def test_send_otlp_invalid_databag( { 123: _OtlpEndpoint( protocol='http', - endpoint='http://provider-123.endpoint:4318', + endpoint='http://provider-123:4318', telemetries=['logs', 'metrics'], ), 456: _OtlpEndpoint( - protocol='grpc', - endpoint='http://provider-456.endpoint:4317', - telemetries=['traces'], + protocol='grpc', endpoint='http://provider-456:4317', telemetries=['traces'] ), }, ), @@ -150,9 +155,7 @@ def test_send_otlp_invalid_databag( ALL_TELEMETRIES, { 456: _OtlpEndpoint( - protocol='grpc', - endpoint='http://provider-456.endpoint:4317', - telemetries=['traces'], + protocol='grpc', endpoint='http://provider-456:4317', telemetries=['traces'] ) }, ), @@ -161,14 +164,10 @@ def test_send_otlp_invalid_databag( ['metrics'], { 123: _OtlpEndpoint( - protocol='http', - endpoint='http://provider-123.endpoint:4318', - telemetries=['metrics'], + protocol='http', endpoint='http://provider-123:4318', telemetries=['metrics'] ), 456: _OtlpEndpoint( - protocol='http', - endpoint='http://provider-456.endpoint:4318', - telemetries=['metrics'], + protocol='http', endpoint='http://provider-456:4318', telemetries=['metrics'] ), }, ), @@ -186,8 +185,9 @@ def test_send_otlp_with_varying_requirer_support( 'endpoints': json.dumps([ { 'protocol': 'http', - 'endpoint': 'http://provider-123.endpoint:4318', + 'endpoint': 'http://provider-123:4318', 'telemetries': ['logs', 'metrics'], + 'insecure': False, } ]) } @@ -195,13 +195,15 @@ def test_send_otlp_with_varying_requirer_support( 'endpoints': json.dumps([ { 'protocol': 'grpc', - 'endpoint': 'http://provider-456.endpoint:4317', + 'endpoint': 'http://provider-456:4317', 'telemetries': ['traces'], + 'insecure': False, }, { 'protocol': 'http', - 'endpoint': 'http://provider-456.endpoint:4318', + 'endpoint': 'http://provider-456:4318', 'telemetries': ['metrics'], + 'insecure': False, }, ]) } @@ -213,8 +215,7 @@ def test_send_otlp_with_varying_requirer_support( # AND WHEN the requirer has varying support for OTLP protocols and telemetries with otlp_requirer_ctx(otlp_requirer_ctx.on.update_status(), state=state) as mgr: - charm_any = cast('Any', mgr.charm) - remote_endpoints = OtlpRequirer(charm_any, SEND, protocols, telemetries).endpoints + remote_endpoints = OtlpRequirer(mgr.charm, SEND, protocols, telemetries).endpoints # THEN the returned endpoints are filtered accordingly assert {k: v.model_dump() for k, v in remote_endpoints.items()} == { @@ -228,8 +229,9 @@ def test_send_otlp(otlp_requirer_ctx: testing.Context[ops.CharmBase]): 'endpoints': json.dumps([ { 'protocol': 'http', - 'endpoint': 'http://provider-123.endpoint:4318', + 'endpoint': 'http://provider-123:4318', 'telemetries': ['logs', 'metrics'], + 'insecure': False, } ]) } @@ -237,27 +239,25 @@ def test_send_otlp(otlp_requirer_ctx: testing.Context[ops.CharmBase]): 'endpoints': json.dumps([ { 'protocol': 'grpc', - 'endpoint': 'http://provider-456.endpoint:4317', + 'endpoint': 'http://provider-456:4317', 'telemetries': ['traces'], + 'insecure': False, }, { 'protocol': 'http', - 'endpoint': 'http://provider-456.endpoint:4318', + 'endpoint': 'http://provider-456:4318', 'telemetries': ['metrics'], + 'insecure': False, }, ]) } expected_endpoints = { 456: _OtlpEndpoint( - protocol='http', - endpoint='http://provider-456.endpoint:4318', - telemetries=['metrics'], + protocol='http', endpoint='http://provider-456:4318', telemetries=['metrics'] ), 123: _OtlpEndpoint( - protocol='http', - endpoint='http://provider-123.endpoint:4318', - telemetries=['logs', 'metrics'], + protocol='http', endpoint='http://provider-123:4318', telemetries=['logs', 'metrics'] ), } @@ -268,8 +268,7 @@ def test_send_otlp(otlp_requirer_ctx: testing.Context[ops.CharmBase]): # AND WHEN otelcol supports a subset of OTLP protocols and telemetries with otlp_requirer_ctx(otlp_requirer_ctx.on.update_status(), state=state) as mgr: - charm_any = cast('Any', mgr.charm) - remote_endpoints = OtlpRequirer(charm_any, SEND, PROTOCOLS, TELEMETRIES).endpoints + remote_endpoints = OtlpRequirer(mgr.charm, SEND, PROTOCOLS, TELEMETRIES).endpoints # THEN the returned endpoints are filtered accordingly assert {k: v.model_dump() for k, v in remote_endpoints.items()} == { @@ -299,6 +298,7 @@ def test_receive_otlp(otlp_provider_ctx: testing.Context[ops.CharmBase]): 'protocol': 'http', 'endpoint': 'http://fqdn:4318', 'telemetries': ['metrics'], + 'insecure': False, } ], } @@ -365,10 +365,37 @@ def test_favor_modern_endpoints( state = State(leader=True) with otlp_requirer_ctx(otlp_requirer_ctx.on.update_status(), state=state) as mgr: # WHEN the requirer selects an endpoint - charm_any = cast('Any', mgr.charm) - result = OtlpRequirer(charm_any, SEND, PROTOCOLS, TELEMETRIES)._favor_modern_endpoints( - endpoints - ) + requirer = OtlpRequirer(mgr.charm, SEND, PROTOCOLS, TELEMETRIES) # THEN the most modern one is chosen - assert result.protocol == expected_protocol + assert requirer._favor_modern_endpoints(endpoints).protocol == expected_protocol + + +def test_add_endpoint_insecure(otlp_requirer_ctx: testing.Context[ops.CharmBase]): + state = State(leader=True) + with otlp_requirer_ctx(otlp_requirer_ctx.on.update_status(), state=state) as mgr: + # GIVEN a provider + provider = OtlpProvider(mgr.charm) + + # WHEN insecure and secure endpoints are added + provider.add_endpoint( + protocol='http', endpoint='http://host:4318', telemetries=['metrics'], insecure=True + ).add_endpoint( + protocol='grpc', endpoint='http://host:4317', telemetries=['traces'], insecure=False + ) + + # THEN their security settings are preserved + assert provider._endpoints == [ + _OtlpEndpoint( + protocol='http', + endpoint='http://host:4318', + telemetries=['metrics'], + insecure=True, + ), + _OtlpEndpoint( + protocol='grpc', + endpoint='http://host:4317', + telemetries=['traces'], + insecure=False, + ), + ] diff --git a/interfaces/otlp/tests/unit/test_rules.py b/interfaces/otlp/tests/unit/test_rules.py index 0b8638c2..212ddcb0 100644 --- a/interfaces/otlp/tests/unit/test_rules.py +++ b/interfaces/otlp/tests/unit/test_rules.py @@ -4,7 +4,7 @@ """Feature: Rules aggregation and labeling.""" import json -from typing import Any, cast +from typing import Any import ops import pytest @@ -92,11 +92,10 @@ def test_duplicate_rules_per_unit( ): with otlp_requirer_ctx(otlp_requirer_ctx.on.update_status(), state=State(leader=True)) as mgr: # GIVEN any charm - charm_any = cast('Any', mgr.charm) # WHEN the OtlpRequirer is initialized # * generic aggregator rules are desired # * no peer relation name is provided - result = OtlpRequirer(charm_any)._duplicate_rules_per_unit( + result = OtlpRequirer(mgr.charm)._duplicate_rules_per_unit( alert_rules={ 'groups': [ { @@ -240,9 +239,8 @@ def test_provider_rules( state = State(leader=True, relations=[receiver], model=MODEL) with otlp_provider_ctx(otlp_provider_ctx.on.update_status(), state=state) as mgr: # WHEN the provider aggregates the rules from the databag - charm_any = cast('Any', mgr.charm) - logql = OtlpProvider(charm_any, RECEIVE).rules('logql') - promql = OtlpProvider(charm_any, RECEIVE).rules('promql') + logql = OtlpProvider(mgr.charm, RECEIVE).rules('logql') + promql = OtlpProvider(mgr.charm, RECEIVE).rules('promql') assert logql assert promql for result in [logql, promql]: