gluetool 3.1__tar.gz → 3.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. {gluetool-3.1 → gluetool-3.3}/PKG-INFO +1 -2
  2. {gluetool-3.1 → gluetool-3.3}/gluetool/action.py +65 -73
  3. {gluetool-3.1 → gluetool-3.3}/gluetool/log.py +8 -8
  4. gluetool-3.3/gluetool/tests/test_action.py +109 -0
  5. {gluetool-3.1 → gluetool-3.3}/pyproject.toml +7 -5
  6. gluetool-3.1/gluetool/core.moduleinfo +0 -33
  7. gluetool-3.1/gluetool_modules/dep_list.py +0 -285
  8. {gluetool-3.1 → gluetool-3.3}/README.rst +0 -0
  9. {gluetool-3.1 → gluetool-3.3}/assets/html-log/prism.css +0 -0
  10. {gluetool-3.1 → gluetool-3.3}/assets/html-log/prism.js +0 -0
  11. {gluetool-3.1 → gluetool-3.3}/assets/html-log/semantic.min.css +0 -0
  12. {gluetool-3.1 → gluetool-3.3}/assets/html-log/semantic.min.js +0 -0
  13. {gluetool-3.1 → gluetool-3.3}/gluetool/__init__.py +0 -0
  14. {gluetool-3.1 → gluetool-3.3}/gluetool/color.py +0 -0
  15. {gluetool-3.1 → gluetool-3.3}/gluetool/glue.py +0 -0
  16. {gluetool-3.1 → gluetool-3.3}/gluetool/help.py +0 -0
  17. {gluetool-3.1 → gluetool-3.3}/gluetool/html_log.py +0 -0
  18. {gluetool-3.1 → gluetool-3.3}/gluetool/proxy.py +0 -0
  19. {gluetool-3.1 → gluetool-3.3}/gluetool/py.typed +0 -0
  20. {gluetool-3.1 → gluetool-3.3}/gluetool/pylint/__init__.py +0 -0
  21. {gluetool-3.1 → gluetool-3.3}/gluetool/pylint/option_default.py +0 -0
  22. {gluetool-3.1 → gluetool-3.3}/gluetool/pylint/shared_defined.py +0 -0
  23. {gluetool-3.1 → gluetool-3.3}/gluetool/pylint/unknown_option.py +0 -0
  24. {gluetool-3.1 → gluetool-3.3}/gluetool/result.py +0 -0
  25. {gluetool-3.1 → gluetool-3.3}/gluetool/sentry.py +0 -0
  26. {gluetool-3.1 → gluetool-3.3}/gluetool/tests/__init__.py +0 -0
  27. {gluetool-3.1 → gluetool-3.3}/gluetool/tests/assets/parse_config/configroot/config/data_config_root_a +0 -0
  28. {gluetool-3.1 → gluetool-3.3}/gluetool/tests/assets/parse_config/configroot/config/data_config_root_b +0 -0
  29. {gluetool-3.1 → gluetool-3.3}/gluetool/tests/assets/parse_config/configroot/config/data_not_unicode +0 -0
  30. {gluetool-3.1 → gluetool-3.3}/gluetool/tests/assets/parse_config/configroota/config/data_config_root_a +0 -0
  31. {gluetool-3.1 → gluetool-3.3}/gluetool/tests/assets/parse_config/configrootb/config/data_config_root_b +0 -0
  32. {gluetool-3.1 → gluetool-3.3}/gluetool/tests/conftest.py +0 -0
  33. {gluetool-3.1 → gluetool-3.3}/gluetool/tests/test_core.py +0 -0
  34. {gluetool-3.1 → gluetool-3.3}/gluetool/tests/test_error.py +0 -0
  35. {gluetool-3.1 → gluetool-3.3}/gluetool/tests/test_eval_context.py +0 -0
  36. {gluetool-3.1 → gluetool-3.3}/gluetool/tests/test_help.py +0 -0
  37. {gluetool-3.1 → gluetool-3.3}/gluetool/tests/test_json.py +0 -0
  38. {gluetool-3.1 → gluetool-3.3}/gluetool/tests/test_load_yaml.py +0 -0
  39. {gluetool-3.1 → gluetool-3.3}/gluetool/tests/test_log_exception.py +0 -0
  40. {gluetool-3.1 → gluetool-3.3}/gluetool/tests/test_logging.py +0 -0
  41. {gluetool-3.1 → gluetool-3.3}/gluetool/tests/test_module_discovery.py +0 -0
  42. {gluetool-3.1 → gluetool-3.3}/gluetool/tests/test_new_xml_element.py +0 -0
  43. {gluetool-3.1 → gluetool-3.3}/gluetool/tests/test_normalize_option.py +0 -0
  44. {gluetool-3.1 → gluetool-3.3}/gluetool/tests/test_option.py +0 -0
  45. {gluetool-3.1 → gluetool-3.3}/gluetool/tests/test_parse_config.py +0 -0
  46. {gluetool-3.1 → gluetool-3.3}/gluetool/tests/test_pipeline_step.py +0 -0
  47. {gluetool-3.1 → gluetool-3.3}/gluetool/tests/test_render_template.py +0 -0
  48. {gluetool-3.1 → gluetool-3.3}/gluetool/tests/test_requests.py +0 -0
  49. {gluetool-3.1 → gluetool-3.3}/gluetool/tests/test_result.py +0 -0
  50. {gluetool-3.1 → gluetool-3.3}/gluetool/tests/test_run_command.py +0 -0
  51. {gluetool-3.1 → gluetool-3.3}/gluetool/tests/test_run_modules.py +0 -0
  52. {gluetool-3.1 → gluetool-3.3}/gluetool/tests/test_tool.py +0 -0
  53. {gluetool-3.1 → gluetool-3.3}/gluetool/tests/test_treat_url.py +0 -0
  54. {gluetool-3.1 → gluetool-3.3}/gluetool/tests/test_utils.py +0 -0
  55. {gluetool-3.1 → gluetool-3.3}/gluetool/tests/test_wait.py +0 -0
  56. {gluetool-3.1 → gluetool-3.3}/gluetool/tool.py +0 -0
  57. {gluetool-3.1 → gluetool-3.3}/gluetool/utils.py +0 -0
  58. {gluetool-3.1 → gluetool-3.3}/gluetool/version.py +0 -0
  59. {gluetool-3.1 → gluetool-3.3}/gluetool_modules/__init__.py +0 -0
  60. {gluetool-3.1 → gluetool-3.3}/gluetool_modules/bash_completion.py +0 -0
  61. {gluetool-3.1 → gluetool-3.3}/gluetool_modules/yaml_pipeline.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: gluetool
3
- Version: 3.1
3
+ Version: 3.3
4
4
  Summary: Python framework for constructing command-line pipelines.
5
5
  Home-page: https://gluetool.readthedocs.org/
6
6
  License: BSD
@@ -30,7 +30,6 @@ Requires-Dist: docutils
30
30
  Requires-Dist: lxml
31
31
  Requires-Dist: mock
32
32
  Requires-Dist: mypy-extensions
33
- Requires-Dist: packaging
34
33
  Requires-Dist: psutil
35
34
  Requires-Dist: requests
36
35
  Requires-Dist: requests-toolbelt
@@ -3,9 +3,9 @@ Actions are pieces of workflow. Actions have a name and start/end time, and acti
3
3
 
4
4
  This module provides simple instrumentation to enable logging of actions, and - when client
5
5
  libraries are installed - submission of actions to storages compatible with
6
- <https://opentracing.io/docs/overview/>`_.
6
+ `OpenTelemetry <https://opentelemetry.io/>`_.
7
7
 
8
- Actions - which are wrapping Open Tracing's _span_ objects - can form an acyclic graph - actions
8
+ Actions - which are wrapping OpenTelemetry's _span_ objects - can form an acyclic graph - actions
9
9
  can have a single parent, it is then possible to start with a "root" action (e.g. "running the pipeline")
10
10
  from which are children actions spawned (e.g. "executing module") and these actions can become parents
11
11
  to actions spawned deeper in modules, end so on and on, forming a nice overview of what the code did, how
@@ -34,34 +34,43 @@ When a supported tracing client is installed, spans created by actions actions a
34
34
 
35
35
  Supported clients:
36
36
 
37
- * https://github.com/jaegertracing/jaeger-client-python
37
+ * `OpenTelemetry SDK <https://opentelemetry-python.readthedocs.io/>`_ with OTLP exporter
38
38
 
39
39
  To control behavior of tracing subsystem, you can use following environment variables:
40
40
 
41
41
  * ``GLUETOOL_TRACING_DISABLE`` - when set to anything, tracing won't be enabled even when a client
42
42
  is available.
43
43
  * ``GLUETOOL_TRACING_SERVICE_NAME`` - given string is used to name the trace produced by ``gluetool`` execution.
44
- * ``GLUETOOL_TRACING_REPORTING_HOST`` - given string represents a hostname where service, capturing
45
- traces, listens.
46
- * ``GLUETOOL_TRACING_REPORTING_PORT`` - given integer represents a port number where service, capturing
47
- traces, listens.
44
+ * ``GLUETOOL_TRACING_REPORTING_ENDPOINT`` - OTLP endpoint URL (e.g. ``http://localhost:4317``).
45
+ * ``GLUETOOL_TRACING_INSECURE`` - controls whether the gRPC connection uses TLS. The connection
46
+ is insecure (plaintext) by default. Set to ``0``, ``false``, or ``no`` to enable TLS.
47
+ * ``GLUETOOL_TRACING_TLS_CERT`` - path to a custom CA certificate bundle (PEM) for TLS connections.
48
+ Only used when ``GLUETOOL_TRACING_INSECURE`` is disabled.
48
49
  """
49
50
 
50
51
  import datetime
51
52
  import os
52
53
  import threading
53
- import time
54
54
 
55
55
  from six import iteritems
56
56
 
57
57
  from .log import Logging
58
- from .result import Result
59
58
 
60
59
  try:
61
- import jaeger_client as tracing_client
60
+ if os.getenv('GLUETOOL_TRACING_DISABLE'):
61
+ raise ImportError('Tracing is disabled')
62
+
63
+ import grpc
64
+ from opentelemetry import trace
65
+ from opentelemetry.sdk.trace import TracerProvider
66
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
67
+ from opentelemetry.sdk.resources import Resource
68
+ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
69
+
70
+ HAS_TRACING = True
62
71
 
63
72
  except ImportError:
64
- tracing_client = None
73
+ HAS_TRACING = False
65
74
 
66
75
  # Type annotations
67
76
  # pylint: disable=unused-import, wrong-import-order
@@ -69,18 +78,16 @@ from typing import cast, Any, Dict, Generator, Iterator, List, Optional, Union
69
78
 
70
79
  from .log import ContextAdapter # noqa
71
80
 
72
- TracingClientType = Any # pylint: disable=invalid-name
73
-
74
81
 
75
82
  TRACING_DISABLE_ENVVAR = 'GLUETOOL_TRACING_DISABLE'
76
83
  TRACING_SERVICE_NAME_ENVVAR = 'GLUETOOL_TRACING_SERVICE_NAME'
77
- TRACING_REPORTING_HOST_ENVVAR = 'GLUETOOL_TRACING_REPORTING_HOST'
78
- TRACING_REPORTING_PORT_ENVVAR = 'GLUETOOL_TRACING_REPORTING_PORT'
84
+ TRACING_REPORTING_ENDPOINT_ENVVAR = 'GLUETOOL_TRACING_REPORTING_ENDPOINT'
79
85
  TRACING_FLUSH_TIMEOUT_ENVVAR = 'GLUETOOL_TRACING_FLUSH_TIMEOUT'
86
+ TRACING_INSECURE_ENVVAR = 'GLUETOOL_TRACING_INSECURE'
87
+ TRACING_TLS_CERT_ENVVAR = 'GLUETOOL_TRACING_TLS_CERT'
80
88
 
81
89
  DEFAULT_TRACING_SERVICE_NAME = 'gluetool'
82
- DEFAULT_TRACING_REPORTING_HOST = '127.0.0.1'
83
- DEFAULT_TRACING_REPORTING_PORT = 5775
90
+ DEFAULT_TRACING_REPORTING_ENDPOINT = 'http://localhost:4317'
84
91
  DEFAULT_TRACING_FLUSH_TIMEOUT = 30
85
92
 
86
93
 
@@ -90,54 +97,50 @@ class Tracer(object):
90
97
 
91
98
  :param str service_name: name to apply to all traces produced by this tracer.
92
99
  :param ContextAdapter logger: logger to use for logging.
93
- :param str reporting_host: address to which tracer should submit traces.
94
- :param int reporting_port: port to which tracer should submit tracers.
100
+ :param str endpoint: OTLP endpoint URL to which tracer should submit traces.
95
101
  """
96
102
  # pylint: disable=too-few-public-methods
97
103
 
98
- TRACER: Optional[TracingClientType] = None
104
+ TRACER: Optional[Any] = None
105
+ _PROVIDER: Optional[Any] = None
99
106
 
100
- # pylint: disable=too-many-arguments
101
107
  def __init__(self,
102
108
  service_name: Optional[str] = None,
103
109
  logger: Optional[ContextAdapter] = None,
104
- reporting_host: Optional[str] = None,
105
- reporting_port: Optional[int] = None) -> None:
110
+ endpoint: Optional[str] = None) -> None:
106
111
 
107
- if not tracing_client or os.getenv(TRACING_DISABLE_ENVVAR):
112
+ if not HAS_TRACING or os.getenv(TRACING_DISABLE_ENVVAR):
108
113
  return
109
114
 
110
115
  self.logger = logger or Logging.get_logger()
111
116
 
112
- if not service_name:
113
- service_name = os.getenv(TRACING_SERVICE_NAME_ENVVAR, default=DEFAULT_TRACING_SERVICE_NAME)
117
+ resolved_service_name: str = service_name or os.getenv(
118
+ TRACING_SERVICE_NAME_ENVVAR, DEFAULT_TRACING_SERVICE_NAME
119
+ ) or DEFAULT_TRACING_SERVICE_NAME
114
120
 
115
- if not reporting_host:
116
- reporting_host = os.getenv(TRACING_REPORTING_HOST_ENVVAR, default=DEFAULT_TRACING_REPORTING_HOST)
121
+ resolved_endpoint: str = endpoint or os.getenv(
122
+ TRACING_REPORTING_ENDPOINT_ENVVAR, DEFAULT_TRACING_REPORTING_ENDPOINT
123
+ ) or DEFAULT_TRACING_REPORTING_ENDPOINT
117
124
 
118
- if not reporting_port:
119
- # pylint: disable=bad-option-value,invalid-envvar-default
120
- reporting_port = int(os.getenv(TRACING_REPORTING_PORT_ENVVAR, default=DEFAULT_TRACING_REPORTING_PORT))
121
-
122
- config = tracing_client.Config(
123
- config={
124
- # Using `const` sampler - the same sampling decission for all spans,
125
- # and that decision is "record" (because `param == 1`).
126
- 'sampler': {
127
- 'type': 'const',
128
- 'param': 1
129
- },
130
- 'local_agent': {
131
- 'reporting_host': reporting_host,
132
- 'reporting_port': reporting_port
133
- },
134
- 'logging': True
135
- },
136
- service_name=service_name,
137
- validate=True
138
- )
125
+ insecure = os.getenv(TRACING_INSECURE_ENVVAR, '').lower() not in ('0', 'false', 'no')
126
+ tls_cert = os.getenv(TRACING_TLS_CERT_ENVVAR)
127
+
128
+ resource = Resource.create({'service.name': resolved_service_name})
129
+ provider = TracerProvider(resource=resource)
130
+
131
+ if insecure:
132
+ exporter = OTLPSpanExporter(endpoint=resolved_endpoint, insecure=True)
133
+ elif tls_cert:
134
+ with open(tls_cert, 'rb') as f:
135
+ credentials = grpc.ssl_channel_credentials(root_certificates=f.read())
136
+ exporter = OTLPSpanExporter(endpoint=resolved_endpoint, credentials=credentials)
137
+ else:
138
+ exporter = OTLPSpanExporter(endpoint=resolved_endpoint)
139
+ provider.add_span_processor(BatchSpanProcessor(exporter))
140
+ trace.set_tracer_provider(provider)
139
141
 
140
- Tracer.TRACER = config.initialize_tracer()
142
+ Tracer._PROVIDER = provider
143
+ Tracer.TRACER = trace.get_tracer(resolved_service_name)
141
144
 
142
145
  def close(self, flush_timeout: Optional[int] = None, logger: Optional[ContextAdapter] = None) -> None:
143
146
  """
@@ -148,31 +151,19 @@ class Tracer(object):
148
151
  :param ContextAdapter logger: logger to use for logging.
149
152
  """.format(TRACING_FLUSH_TIMEOUT_ENVVAR, DEFAULT_TRACING_FLUSH_TIMEOUT)
150
153
 
151
- if not Tracer.TRACER:
154
+ if not Tracer._PROVIDER:
152
155
  return
153
156
 
154
157
  logger = logger or self.logger
155
158
 
156
159
  logger.info('Flushing tracing data')
157
160
 
158
- # Make pylint happy about circular imports by not using global import.
159
- # pylint: disable=cyclic-import
160
- from .utils import wait
161
-
162
161
  if not flush_timeout:
163
162
  # pylint: disable=bad-option-value,invalid-envvar-default
164
163
  flush_timeout = int(os.getenv(TRACING_FLUSH_TIMEOUT_ENVVAR, default=DEFAULT_TRACING_FLUSH_TIMEOUT))
165
164
 
166
- # yield to IOLoop to flush the spans - https://github.com/jaegertracing/jaeger-client-python/issues/50
167
- time.sleep(2)
168
-
169
- future = Tracer.TRACER.close()
170
-
171
- def _check_flush() -> Result[bool, str]:
172
-
173
- return Result.Ok(True) if future.done() else Result.Error('flush pending')
174
-
175
- wait('tracing flush', _check_flush, timeout=flush_timeout, tick=2, logger=logger)
165
+ Tracer._PROVIDER.force_flush(timeout_millis=flush_timeout * 1000)
166
+ Tracer._PROVIDER.shutdown()
176
167
 
177
168
 
178
169
  class Action(object):
@@ -284,14 +275,15 @@ class Action(object):
284
275
 
285
276
  self.tags = tags or {}
286
277
 
287
- if Tracer.TRACER:
288
- if parent:
289
- parent_span = parent.span
290
-
278
+ if Tracer.TRACER and HAS_TRACING:
279
+ if parent and parent.span:
280
+ parent_ctx = trace.set_span_in_context(parent.span)
291
281
  else:
292
- parent_span = None
282
+ parent_ctx = None
293
283
 
294
- self.span: Any = Tracer.TRACER.start_span(label, child_of=parent_span, tags=tags)
284
+ self.span: Any = Tracer.TRACER.start_span(
285
+ label, context=parent_ctx, attributes=tags
286
+ )
295
287
 
296
288
  else:
297
289
  self.span = None
@@ -320,7 +312,7 @@ class Action(object):
320
312
  Action._drop_action(self)
321
313
 
322
314
  if self.span:
323
- self.span.finish()
315
+ self.span.end()
324
316
 
325
317
  self.logger.debug("action '{}', child of '{}', with span '{}', finished at {}".format(
326
318
  self.label,
@@ -342,7 +334,7 @@ class Action(object):
342
334
  self.tags[name] = value
343
335
 
344
336
  if self.span:
345
- self.span.set_tag(name, value)
337
+ self.span.set_attribute(name, value)
346
338
 
347
339
  def set_tags(self, tags: Dict[str, Any]) -> None:
348
340
 
@@ -1132,17 +1132,17 @@ class Logging(object):
1132
1132
  assert Logging.stderr_handler is not None
1133
1133
  logger.addHandler(Logging.stderr_handler)
1134
1134
 
1135
- # Extra handling for Jaeger logger:
1136
- # - replace it with a context adapter to keep track of what's being done by Jaeger code,
1137
- # - patch its `info` with `debug` - Jaeger is logging things with INFO severity, which spoils
1135
+ # Extra handling for OpenTelemetry logger:
1136
+ # - replace it with a context adapter to keep track of what's being done by tracing code,
1137
+ # - patch its `info` with `debug` - OpenTelemetry logs with INFO severity, which spoils
1138
1138
  # our output, DEBUG is perfectly fine for us.
1139
- jaeger_logger = logging.getLogger('jaeger_tracing')
1139
+ otel_logger = logging.getLogger('opentelemetry')
1140
1140
 
1141
- if logger == jaeger_logger:
1142
- jaeger_context_adapter = PackageAdapter(Logging.get_logger(), 'tracing')
1141
+ if logger == otel_logger:
1142
+ otel_context_adapter = PackageAdapter(Logging.get_logger(), 'tracing')
1143
1143
 
1144
1144
  for attr in ('debug', 'info', 'warning', 'error', 'exception'):
1145
- setattr(logger, attr, getattr(jaeger_context_adapter, attr))
1145
+ setattr(logger, attr, getattr(otel_context_adapter, attr))
1146
1146
 
1147
1147
  logger.info = logger.debug # type: ignore
1148
1148
 
@@ -1172,7 +1172,7 @@ class Logging(object):
1172
1172
 
1173
1173
  OUR_LOGGERS = (
1174
1174
  logging.getLogger('gluetool'),
1175
- logging.getLogger('jaeger_tracing'),
1175
+ logging.getLogger('opentelemetry'),
1176
1176
  logging.getLogger('urllib3')
1177
1177
  )
1178
1178
 
@@ -0,0 +1,109 @@
1
+ import os
2
+
3
+ import pytest
4
+ from unittest.mock import patch, MagicMock
5
+
6
+ import gluetool.action
7
+ from gluetool.action import (
8
+ TRACING_INSECURE_ENVVAR,
9
+ TRACING_TLS_CERT_ENVVAR,
10
+ Tracer,
11
+ )
12
+
13
+
14
+ @pytest.fixture(autouse=True)
15
+ def _reset_tracer():
16
+ """Reset Tracer class state between tests."""
17
+ Tracer.TRACER = None
18
+ Tracer._PROVIDER = None
19
+ yield
20
+ Tracer.TRACER = None
21
+ Tracer._PROVIDER = None
22
+
23
+
24
+ @pytest.fixture()
25
+ def mock_otel():
26
+ """Mock all OpenTelemetry objects used by Tracer.__init__."""
27
+ with patch.object(gluetool.action, 'HAS_TRACING', True), \
28
+ patch.object(gluetool.action, 'Resource'), \
29
+ patch.object(gluetool.action, 'TracerProvider'), \
30
+ patch.object(gluetool.action, 'BatchSpanProcessor'), \
31
+ patch.object(gluetool.action, 'OTLPSpanExporter') as mock_exporter_cls, \
32
+ patch.object(gluetool.action, 'trace'), \
33
+ patch.object(gluetool.action, 'grpc', create=True) as mock_grpc:
34
+ yield {
35
+ 'exporter_cls': mock_exporter_cls,
36
+ 'grpc': mock_grpc,
37
+ }
38
+
39
+
40
+ @pytest.mark.parametrize('env_value', [
41
+ '', # not set
42
+ 'true',
43
+ 'yes',
44
+ '1',
45
+ 'anything',
46
+ ])
47
+ def test_insecure_by_default(env_value, mock_otel):
48
+ """Connection is insecure when GLUETOOL_TRACING_INSECURE is unset or not explicitly disabled."""
49
+
50
+ env = {TRACING_INSECURE_ENVVAR: env_value} if env_value else {}
51
+
52
+ with patch.dict(os.environ, env, clear=False):
53
+ if not env_value:
54
+ os.environ.pop(TRACING_INSECURE_ENVVAR, None)
55
+ Tracer()
56
+
57
+ _, kwargs = mock_otel['exporter_cls'].call_args
58
+ assert kwargs.get('insecure') is True
59
+
60
+
61
+ @pytest.mark.parametrize('env_value', ['0', 'false', 'no', 'False', 'NO', 'No'])
62
+ def test_secure_when_insecure_disabled(env_value, mock_otel):
63
+ """Connection uses TLS when GLUETOOL_TRACING_INSECURE is set to 0/false/no."""
64
+
65
+ with patch.dict(os.environ, {TRACING_INSECURE_ENVVAR: env_value}, clear=False):
66
+ os.environ.pop(TRACING_TLS_CERT_ENVVAR, None)
67
+ Tracer()
68
+
69
+ _, kwargs = mock_otel['exporter_cls'].call_args
70
+ assert 'insecure' not in kwargs
71
+ assert 'credentials' not in kwargs
72
+
73
+
74
+ def test_tls_cert_used_when_secure(mock_otel, tmp_path):
75
+ """Custom CA cert is loaded and passed as gRPC credentials when TLS is enabled."""
76
+
77
+ cert_content = b'--- FAKE CA CERT ---'
78
+ cert_file = tmp_path / 'ca.pem'
79
+ cert_file.write_bytes(cert_content)
80
+
81
+ mock_creds = MagicMock()
82
+ mock_otel['grpc'].ssl_channel_credentials.return_value = mock_creds
83
+
84
+ with patch.dict(os.environ, {
85
+ TRACING_INSECURE_ENVVAR: 'false',
86
+ TRACING_TLS_CERT_ENVVAR: str(cert_file),
87
+ }, clear=False):
88
+ Tracer()
89
+
90
+ mock_otel['grpc'].ssl_channel_credentials.assert_called_once_with(root_certificates=cert_content)
91
+ _, kwargs = mock_otel['exporter_cls'].call_args
92
+ assert kwargs.get('credentials') is mock_creds
93
+
94
+
95
+ def test_tls_cert_ignored_when_insecure(mock_otel, tmp_path):
96
+ """GLUETOOL_TRACING_TLS_CERT is ignored when connection is insecure."""
97
+
98
+ cert_file = tmp_path / 'ca.pem'
99
+ cert_file.write_bytes(b'--- FAKE CA CERT ---')
100
+
101
+ with patch.dict(os.environ, {
102
+ TRACING_TLS_CERT_ENVVAR: str(cert_file),
103
+ }, clear=False):
104
+ os.environ.pop(TRACING_INSECURE_ENVVAR, None)
105
+ Tracer()
106
+
107
+ mock_otel['grpc'].ssl_channel_credentials.assert_not_called()
108
+ _, kwargs = mock_otel['exporter_cls'].call_args
109
+ assert kwargs.get('insecure') is True
@@ -4,7 +4,7 @@ dynamic = ["version"]
4
4
 
5
5
  [tool.poetry]
6
6
  name = "gluetool"
7
- version = "3.1"
7
+ version = "3.3"
8
8
  description = "Python framework for constructing command-line pipelines."
9
9
  authors = [
10
10
  "Milos Prchlik <mprchlik@redhat.com>",
@@ -42,7 +42,6 @@ include = [
42
42
  "assets/html-log/semantic.min.js",
43
43
  "gluetool_modules/__init__.py",
44
44
  "gluetool_modules/bash_completion.py",
45
- "gluetool_modules/dep_list.py",
46
45
  "gluetool_modules/yaml_pipeline.py"
47
46
  ]
48
47
 
@@ -67,7 +66,6 @@ urlnormalizer = "*"
67
66
  # depending on extended argument annotations.
68
67
  mypy-extensions = "*"
69
68
  zipp = "*"
70
- packaging = "*"
71
69
  MarkupSafe = "*"
72
70
  attrs = "*"
73
71
  cattrs = "*"
@@ -102,11 +100,15 @@ gluetool-html-log = "gluetool.html_log:main"
102
100
 
103
101
  [tool.poetry.plugins."gluetool.modules"]
104
102
  ".bash_completion" = "gluetool_modules.bash_completion:BashCompletion"
105
- ".dep_list" = "gluetool_modules.dep_list:DepList"
106
103
  ".yaml_pipeline" = "gluetool_modules.yaml_pipeline:YAMLPipeline"
107
104
 
105
+
106
+ [tool.poetry.group.dev.dependencies]
107
+ opentelemetry-sdk = "^1.40.0"
108
+ opentelemetry-exporter-otlp-proto-grpc = "^1.40.0"
109
+
108
110
  [tool.poetry-dynamic-versioning]
109
- enable = false
111
+ enable = true
110
112
 
111
113
  [build-system]
112
114
  requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"]
@@ -1,33 +0,0 @@
1
- ---
2
- - name: citool-core
3
- description: Continuous Integration Swiss Army Knife
4
- dependencies:
5
- yum:
6
- - krb5-devel
7
- pip:
8
- - alabaster==0.7.10
9
- - Babel==2.5.1
10
- - beautifulsoup4==4.5.3
11
- - certifi==2017.7.27.1
12
- - chardet==3.0.4
13
- - colorama==0.3.7
14
- - contextlib2==0.5.5
15
- - docutils==0.13.1
16
- - enum34==1.1.6
17
- - idna==2.6
18
- - imagesize==0.7.1
19
- - Jinja2==2.9.6
20
- - Mako==1.0.6
21
- - MarkupSafe==1.0
22
- - Pygments==2.2.0
23
- - pytz==2017.2
24
- - raven==6.0.0
25
- - requests==2.18.4
26
- - ruamel.ordereddict==0.4.13
27
- - ruamel.yaml==0.15.34
28
- - six==1.11.0
29
- - snowballstemmer==1.2.1
30
- - Sphinx==1.5.2
31
- - tabulate==0.7.7
32
- - urllib3==1.22
33
- - urlnorm==1.1.4
@@ -1,285 +0,0 @@
1
- import os
2
- import operator
3
-
4
- from packaging.version import Version
5
-
6
- from six import iteritems, itervalues
7
-
8
- import gluetool
9
-
10
-
11
- class ModuleInfo(object):
12
- # pylint: disable=too-few-public-methods
13
- def __init__(self, data):
14
- self.name = data['name']
15
- self.description = data.get('description', '')
16
- self.dependencies = data.get('dependencies', {})
17
- deps = self.dependencies
18
-
19
- self.repo = self.normalize_repos(deps.get('repo', []))
20
- self.yum = deps.get('yum', [])
21
- self.pip = deps.get('pip', [])
22
- self.tasks = deps.get('ansible_tasks', [])
23
-
24
- @staticmethod
25
- def normalize_repos(repos):
26
- for repo in repos:
27
- if 'name' not in repo:
28
- repo['name'] = 'repo' + str(id(repo))
29
- return repos
30
-
31
-
32
- class ModuleInfoGroup(object):
33
- def __init__(self, logger):
34
- self.items = {}
35
- self.logger = logger
36
- self.ops_map = {
37
- '>': operator.gt,
38
- '<': operator.lt,
39
- '>=': operator.ge,
40
- '<=': operator.le,
41
- '==': operator.eq
42
- }
43
-
44
- def add_moduleinfo(self, moduleinfo):
45
- if moduleinfo.name in self.items:
46
- self.logger.warn("Module '{}' is already loaded".format(moduleinfo.name))
47
- return False
48
-
49
- self.items[moduleinfo.name] = moduleinfo
50
- return True
51
-
52
- def get_dependencies(self, only_modules):
53
- data = {
54
- 'repo': [],
55
- 'yum': [],
56
- 'pip': [],
57
- 'ansible_tasks': []
58
- }
59
- for moduleinfo in itervalues(self.items):
60
- if only_modules and moduleinfo.name not in only_modules:
61
- self.logger.debug("Skip module '{}'".format(moduleinfo.name))
62
- continue
63
-
64
- self.logger.info('Collect dependencies for \'{}\''.format(moduleinfo.name))
65
- if moduleinfo.yum:
66
- data['yum'].extend(moduleinfo.yum)
67
- if moduleinfo.pip:
68
- data['pip'].extend(moduleinfo.pip)
69
- if moduleinfo.repo:
70
- data['repo'].extend(moduleinfo.repo)
71
- if moduleinfo.tasks:
72
- data['ansible_tasks'].extend(moduleinfo.tasks)
73
-
74
- self.logger.debug('pip dependencies:\n{}'.format(data['pip']))
75
- data['pip'] = self.pip_version_unify(data['pip'])
76
-
77
- return data
78
-
79
- def pip_version_unify(self, pip_deps):
80
- result = []
81
- versions = {}
82
- for item in pip_deps:
83
- pkg, oper, version = ModuleInfoGroup.parse_pkgver(item)
84
- if pkg in versions:
85
- version_limit = versions[pkg]
86
- else:
87
- version_limit = gluetool.utils.Bunch(pkg=pkg, equal=None, lower=None, upper=None)
88
- versions[pkg] = version_limit
89
- if oper and version:
90
- self.logger.debug(item)
91
- self.limit_version(version_limit, oper, version)
92
- self.logger.debug(versions)
93
- for _, item in iteritems(versions):
94
- result.append(self.get_allowed_version_bounds(item))
95
- return result
96
-
97
- def get_allowed_version_bounds(self, item):
98
- if not item.equal and not item.lower and not item.upper:
99
- return item.pkg
100
- if item.equal:
101
- if item.upper:
102
- oper, version = item.upper
103
- relate = self.ops_map[oper]
104
- if not relate(Version(item.equal), Version(version)):
105
- raise gluetool.GlueError("Cannot find common version for package '{}'".format(item.pkg))
106
- if item.lower:
107
- oper, version = item.lower
108
- relate = self.ops_map[oper]
109
- if not relate(Version(item.equal), Version(version)):
110
- raise gluetool.GlueError("Cannot find common version for package '{}'".format(item.pkg))
111
- return '{}=={}'.format(item.pkg, item.equal)
112
- if item.upper and item.lower:
113
- lower_operator, lower_version = item.lower
114
- upper_operator, upper_version = item.upper
115
- relate = self.ops_map[lower_operator]
116
- if not relate(Version(upper_version), Version(lower_version)):
117
- raise gluetool.GlueError("Cannot find common version for package '{}'".format(item.pkg))
118
- relate = self.ops_map[upper_operator]
119
- if not relate(Version(lower_version), Version(upper_version)):
120
- raise gluetool.GlueError("Cannot find common version for package '{}'".format(item.pkg))
121
- return '{}{}{},{}{}'.format(item.pkg, lower_operator, lower_version, upper_operator, upper_version)
122
- if item.upper:
123
- upper_operator, upper_version = item.upper
124
- return '{}{}{}'.format(item.pkg, upper_operator, upper_version)
125
- if item.lower:
126
- lower_operator, lower_version = item.lower
127
- return '{}{}{}'.format(item.pkg, lower_operator, lower_version)
128
-
129
- @staticmethod
130
- def limit_version(item, oper, version):
131
- if oper in ['>', '>=']:
132
- if item.lower:
133
- _, saved_version = item.lower
134
- if Version(saved_version) < Version(version):
135
- item.lower = [oper, version]
136
- elif Version(saved_version) == Version(version) and oper == '>':
137
- item.lower = [oper, version]
138
- else:
139
- item.lower = [oper, version]
140
- elif oper in ['<', '<=']:
141
- if item.upper:
142
- _, saved_version = item.upper
143
- if Version(saved_version) > Version(version):
144
- item.upper = [oper, version]
145
- elif Version(saved_version) == Version(version) and oper == '<':
146
- item.upper = [oper, version]
147
- else:
148
- item.upper = [oper, version]
149
- elif oper in ['==']:
150
- if item.equal and item.equal != version:
151
- raise gluetool.GlueError(
152
- "Different versions '{}' and '{}' of package '{}' required".format(item.equal, version, item.pkg)
153
- )
154
- item.equal = version
155
- else:
156
- raise gluetool.GlueError("Unsupported operator '{}'".format(oper))
157
-
158
- @staticmethod
159
- def parse_pkgver(string):
160
- ops = ['==', '<=', '>=', '<', '>']
161
- for item in ops:
162
- if item in string:
163
- split = string.split(item)
164
- return split[0], item, split[1]
165
- return string, None, None
166
-
167
-
168
- class DepList(gluetool.Module):
169
- """
170
- Module collect dependencies from moduleinfo files.
171
- Type of dependencies:
172
-
173
- repo: list of yum repositories to import on host before yum dependencies are installed
174
- yum: resolved with yum/dnf manager
175
- pip: resolved with pip manager, also version collisions are checked, if specified, \
176
- if more modules specify same package, but only one specify version, this version will be taken
177
- ansible_tasks: list of ansible tasks as you know it from playbooks
178
-
179
- Example of moduleinfo file, file can contain more definitions
180
-
181
- .. code-block:: yaml
182
-
183
- ---
184
-
185
- name: postgresql
186
- description: Connect to PostgreSQL database
187
- dependencies:
188
- yum:
189
- - postgresql
190
- pip:
191
- - psycopg2==2.7.1
192
- repo:
193
- - name: dummyrepo
194
- baseurl: http://dummy.url/release/repo
195
- gpgcheck: 0
196
- ansible_tasks:
197
- - name: ensure file is present
198
- file: path=/etc/magic state=file
199
- """
200
- name = 'dep-list'
201
- description = 'Collect module dependencies for citool modules'
202
- options = {
203
- 'module-dirs': {
204
- 'help': 'Where to find modules'
205
- },
206
- 'only-modules': {
207
- 'help': 'If specified, modules for which dependencies will beresolved'
208
- },
209
- 'output': {
210
- 'help': 'Basename of the output file: ``FILE``-requirements.yml and ``FILE``-tasks.yml will be created.',
211
- 'metavar': 'FILE'
212
- }
213
- }
214
- shared_functions = ['prepare_dependencies']
215
-
216
- @staticmethod
217
- def collect_moduleinfo_files(path):
218
- collection = []
219
- for root, _, files in os.walk(path):
220
- for filename in files:
221
- if filename.endswith('.moduleinfo'):
222
- collection.append(os.path.join(root, filename))
223
- return collection
224
-
225
- def load_moduleinfo_files(self, files):
226
- loaded = ModuleInfoGroup(self.logger)
227
- for filename in files:
228
- yaml = gluetool.utils.load_yaml(filename)
229
- try:
230
- item = ModuleInfo(yaml)
231
- if loaded.add_moduleinfo(item):
232
- self.info("Module info '{}' loaded".format(item.name))
233
- self.debug("Module: '{}'\nDescription: '{}'\nDependencies:\n'{}'".format(
234
- item.name, item.description, gluetool.log.format_dict(item.dependencies)
235
- ))
236
- except KeyError:
237
- self.warn("Module info '{}' cannot be properly loaded".format(filename))
238
- return loaded
239
-
240
- def prepare_dependencies(self, module_dirs, only_modules):
241
- dirs = module_dirs
242
-
243
- files = []
244
- for path in dirs:
245
- self.info("Searching for modules within path '{}'".format(path))
246
- files += self.collect_moduleinfo_files(path)
247
- self.debug(files)
248
- loaded = self.load_moduleinfo_files(files)
249
- dependencies = loaded.get_dependencies(only_modules)
250
- self.debug('Resulting dependencies:\n{}'.format(gluetool.log.format_dict(dependencies)))
251
-
252
- return dependencies
253
-
254
- def execute(self):
255
- dirs = self.option('module-dirs')
256
- if dirs:
257
- dirs = [s.strip() for s in dirs.split(',')]
258
- modules = self.option('only-modules')
259
- if modules:
260
- if modules == '*':
261
- modules = None
262
- else:
263
- modules = [s.strip() for s in modules.split(',')]
264
- outfile = self.option('output')
265
- if dirs and outfile:
266
- dependencies = self.prepare_dependencies(dirs, modules)
267
-
268
- requirements_file, tasks_file = '{}-requirements.yml'.format(outfile), '{}-tasks.yml'.format(outfile)
269
-
270
- gluetool.utils.dump_yaml({
271
- 'compose_requirements': {
272
- 'repositories': dependencies['repo'],
273
- 'pip': dependencies['pip'],
274
- 'yum': dependencies['yum']
275
- }
276
- }, requirements_file)
277
-
278
- self.info("Requirements written into '{}'".format(requirements_file))
279
-
280
- gluetool.utils.dump_yaml(dependencies['ansible_tasks'], tasks_file)
281
-
282
- self.info("Requirements written into '{}'".format(tasks_file))
283
-
284
- else:
285
- self.info('To execute provide --module-dirs and --output, skipping dependencies generation in execute')
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes