Skip to content

Commit 88d684e

Browse files
[DPE-4266] Strip passwords from command execute output and tracebacks (#473)
* Strip passwords from command execute output and tracebacks * Avoid password leak upon ChangeError + do not forward context in _run_mysqlsh_script errors * Remove unnecessary from None for backup exceptions * Update outdated charm libs * Update outdated mysql charm lib
1 parent 2fee0fa commit 88d684e

File tree

9 files changed

+506
-173
lines changed

9 files changed

+506
-173
lines changed

lib/charms/data_platform_libs/v0/data_interfaces.py

Lines changed: 364 additions & 120 deletions
Large diffs are not rendered by default.

lib/charms/mysql/v0/async_replication.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
# The unique Charmhub library identifier, never change it
5555
LIBID = "4de21f1a022c4e2c87ac8e672ec16f6a"
5656
LIBAPI = 0
57-
LIBPATCH = 5
57+
LIBPATCH = 6
5858

5959
RELATION_OFFER = "replication-offer"
6060
RELATION_CONSUMER = "replication"
@@ -778,9 +778,10 @@ def _on_consumer_changed(self, event): # noqa: C901
778778
logger.debug("Recreating cluster prior to sync credentials")
779779
self._charm.create_cluster()
780780
# (re)set flags
781-
self._charm.app_peer_data.update(
782-
{"removed-from-cluster-set": "", "rejoin-secondaries": "true"}
783-
)
781+
self._charm.app_peer_data.update({
782+
"removed-from-cluster-set": "",
783+
"rejoin-secondaries": "true",
784+
})
784785
event.defer()
785786
return
786787
if not self._charm.cluster_fully_initialized:

lib/charms/mysql/v0/backups.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def is_unit_blocked(self) -> bool:
9999

100100
# Increment this PATCH version before using `charmcraft publish-lib` or reset
101101
# to 0 if you are raising the major API version
102-
LIBPATCH = 10
102+
LIBPATCH = 11
103103

104104

105105
if typing.TYPE_CHECKING:
@@ -314,11 +314,9 @@ def _on_create_backup(self, event: ActionEvent) -> None:
314314
return
315315

316316
logger.info(f"Backup succeeded: with backup-id {datetime_backup_requested}")
317-
event.set_results(
318-
{
319-
"backup-id": datetime_backup_requested,
320-
}
321-
)
317+
event.set_results({
318+
"backup-id": datetime_backup_requested,
319+
})
322320
self.charm._on_update_status(None)
323321

324322
def _can_unit_perform_backup(self) -> Tuple[bool, Optional[str]]:
@@ -538,11 +536,9 @@ def _on_restore(self, event: ActionEvent) -> None:
538536
return
539537

540538
logger.info("Restore succeeded")
541-
event.set_results(
542-
{
543-
"completed": "ok",
544-
}
545-
)
539+
event.set_results({
540+
"completed": "ok",
541+
})
546542
# update status as soon as possible
547543
self.charm._on_update_status(None)
548544

lib/charms/mysql/v0/mysql.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ def wait_until_mysql_connection(self) -> None:
134134
# Increment this major API version when introducing breaking changes
135135
LIBAPI = 0
136136

137-
LIBPATCH = 66
137+
LIBPATCH = 67
138138

139139
UNIT_TEARDOWN_LOCKNAME = "unit-teardown"
140140
UNIT_ADD_LOCKNAME = "unit-add"
@@ -874,6 +874,13 @@ def __init__(
874874
self.monitoring_password = monitoring_password
875875
self.backups_user = backups_user
876876
self.backups_password = backups_password
877+
self.passwords = [
878+
self.root_password,
879+
self.server_config_password,
880+
self.cluster_admin_password,
881+
self.monitoring_password,
882+
self.backups_password,
883+
]
877884

878885
def render_mysqld_configuration( # noqa: C901
879886
self,
@@ -2532,7 +2539,7 @@ def execute_backup_commands(
25322539
except Exception:
25332540
# Catch all other exceptions to prevent the database being stuck in
25342541
# a bad state due to pre-backup operations
2535-
logger.exception("Failed to execute commands prior to running backup")
2542+
logger.exception("Failed unexpectedly to execute commands prior to running backup")
25362543
raise MySQLExecuteBackupCommandsError
25372544

25382545
# TODO: remove flags --no-server-version-check
@@ -2582,13 +2589,13 @@ def execute_backup_commands(
25822589
},
25832590
stream_output="stderr",
25842591
)
2585-
except MySQLExecError as e:
2592+
except MySQLExecError:
25862593
logger.exception("Failed to execute backup commands")
2587-
raise MySQLExecuteBackupCommandsError(e.message)
2594+
raise MySQLExecuteBackupCommandsError
25882595
except Exception:
25892596
# Catch all other exceptions to prevent the database being stuck in
25902597
# a bad state due to pre-backup operations
2591-
logger.exception("Failed to execute backup commands")
2598+
logger.exception("Failed unexpectedly to execute backup commands")
25922599
raise MySQLExecuteBackupCommandsError
25932600

25942601
def delete_temp_backup_directory(
@@ -2978,6 +2985,13 @@ def get_non_system_databases(self) -> set[str]:
29782985
"sys",
29792986
}
29802987

2988+
def strip_off_passwords(self, input_string: str) -> str:
2989+
"""Strips off passwords from the input string."""
2990+
stripped_input = input_string
2991+
for password in self.passwords:
2992+
stripped_input = stripped_input.replace(password, "xxxxxxxxxxxx")
2993+
return stripped_input
2994+
29812995
@abstractmethod
29822996
def is_mysqld_running(self) -> bool:
29832997
"""Returns whether mysqld is running."""

lib/charms/mysql/v0/s3_helpers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
"""S3 helper functions for the MySQL charms."""
16+
1617
import base64
1718
import logging
1819
import tempfile
@@ -31,7 +32,7 @@
3132

3233
# Increment this PATCH version before using `charmcraft publish-lib` or reset
3334
# to 0 if you are raising the major API version
34-
LIBPATCH = 8
35+
LIBPATCH = 9
3536

3637
# botocore/urllib3 clutter the logs when on debug
3738
logging.getLogger("botocore").setLevel(logging.WARNING)

lib/charms/mysql/v0/tls.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
2020
"""
2121

22-
2322
import base64
2423
import logging
2524
import re
@@ -52,7 +51,7 @@
5251

5352
LIBID = "eb73947deedd4380a3a90d527e0878eb"
5453
LIBAPI = 0
55-
LIBPATCH = 6
54+
LIBPATCH = 7
5655

5756
SCOPE = "unit"
5857

lib/charms/tempo_k8s/v1/charm_tracing.py

Lines changed: 89 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,59 @@ def my_tracing_endpoint(self) -> Optional[str]:
172172
provide an *absolute* path to the certificate file instead.
173173
"""
174174

175+
176+
def _remove_stale_otel_sdk_packages():
177+
"""Hack to remove stale opentelemetry sdk packages from the charm's python venv.
178+
179+
See https://github.com/canonical/grafana-agent-operator/issues/146 and
180+
https://bugs.launchpad.net/juju/+bug/2058335 for more context. This patch can be removed after
181+
this juju issue is resolved and sufficient time has passed to expect most users of this library
182+
have migrated to the patched version of juju. When this patch is removed, un-ignore rule E402 for this file in the pyproject.toml (see setting
183+
[tool.ruff.lint.per-file-ignores] in pyproject.toml).
184+
185+
This only has an effect if executed on an upgrade-charm event.
186+
"""
187+
# all imports are local to keep this function standalone, side-effect-free, and easy to revert later
188+
import os
189+
190+
if os.getenv("JUJU_DISPATCH_PATH") != "hooks/upgrade-charm":
191+
return
192+
193+
import logging
194+
import shutil
195+
from collections import defaultdict
196+
197+
from importlib_metadata import distributions
198+
199+
otel_logger = logging.getLogger("charm_tracing_otel_patcher")
200+
otel_logger.debug("Applying _remove_stale_otel_sdk_packages patch on charm upgrade")
201+
# group by name all distributions starting with "opentelemetry_"
202+
otel_distributions = defaultdict(list)
203+
for distribution in distributions():
204+
name = distribution._normalized_name # type: ignore
205+
if name.startswith("opentelemetry_"):
206+
otel_distributions[name].append(distribution)
207+
208+
otel_logger.debug(f"Found {len(otel_distributions)} opentelemetry distributions")
209+
210+
# If we have multiple distributions with the same name, remove any that have 0 associated files
211+
for name, distributions_ in otel_distributions.items():
212+
if len(distributions_) <= 1:
213+
continue
214+
215+
otel_logger.debug(f"Package {name} has multiple ({len(distributions_)}) distributions.")
216+
for distribution in distributions_:
217+
if not distribution.files: # Not None or empty list
218+
path = distribution._path # type: ignore
219+
otel_logger.info(f"Removing empty distribution of {name} at {path}.")
220+
shutil.rmtree(path)
221+
222+
otel_logger.debug("Successfully applied _remove_stale_otel_sdk_packages patch. ")
223+
224+
225+
_remove_stale_otel_sdk_packages()
226+
227+
175228
import functools
176229
import inspect
177230
import logging
@@ -197,14 +250,15 @@ def my_tracing_endpoint(self) -> Optional[str]:
197250
from opentelemetry.sdk.resources import Resource
198251
from opentelemetry.sdk.trace import Span, TracerProvider
199252
from opentelemetry.sdk.trace.export import BatchSpanProcessor
200-
from opentelemetry.trace import INVALID_SPAN, Tracer
201-
from opentelemetry.trace import get_current_span as otlp_get_current_span
202253
from opentelemetry.trace import (
254+
INVALID_SPAN,
255+
Tracer,
203256
get_tracer,
204257
get_tracer_provider,
205258
set_span_in_context,
206259
set_tracer_provider,
207260
)
261+
from opentelemetry.trace import get_current_span as otlp_get_current_span
208262
from ops.charm import CharmBase
209263
from ops.framework import Framework
210264

@@ -217,7 +271,7 @@ def my_tracing_endpoint(self) -> Optional[str]:
217271
# Increment this PATCH version before using `charmcraft publish-lib` or reset
218272
# to 0 if you are raising the major API version
219273

220-
LIBPATCH = 11
274+
LIBPATCH = 14
221275

222276
PYDEPS = ["opentelemetry-exporter-otlp-proto-http==1.21.0"]
223277

@@ -391,6 +445,9 @@ def wrap_init(self: CharmBase, framework: Framework, *args, **kwargs):
391445
_service_name = service_name or f"{self.app.name}-charm"
392446

393447
unit_name = self.unit.name
448+
# apply hacky patch to remove stale opentelemetry sdk packages on upgrade-charm.
449+
# it could be trouble if someone ever decides to implement their own tracer parallel to
450+
# ours and before the charm has inited. We assume they won't.
394451
resource = Resource.create(
395452
attributes={
396453
"service.name": _service_name,
@@ -612,38 +669,58 @@ def trace_type(cls: _T) -> _T:
612669
dev_logger.info(f"skipping {method} (dunder)")
613670
continue
614671

615-
new_method = trace_method(method)
616-
if isinstance(inspect.getattr_static(cls, method.__name__), staticmethod):
672+
# the span title in the general case should be:
673+
# method call: MyCharmWrappedMethods.b
674+
# if the method has a name (functools.wrapped or regular method), let
675+
# _trace_callable use its default algorithm to determine what name to give the span.
676+
trace_method_name = None
677+
try:
678+
qualname_c0 = method.__qualname__.split(".")[0]
679+
if not hasattr(cls, method.__name__):
680+
# if the callable doesn't have a __name__ (probably a decorated method),
681+
# it probably has a bad qualname too (such as my_decorator.<locals>.wrapper) which is not
682+
# great for finding out what the trace is about. So we use the method name instead and
683+
# add a reference to the decorator name. Result:
684+
# method call: @my_decorator(MyCharmWrappedMethods.b)
685+
trace_method_name = f"@{qualname_c0}({cls.__name__}.{name})"
686+
except Exception: # noqa: failsafe
687+
pass
688+
689+
new_method = trace_method(method, name=trace_method_name)
690+
691+
if isinstance(inspect.getattr_static(cls, name), staticmethod):
617692
new_method = staticmethod(new_method)
618693
setattr(cls, name, new_method)
619694

620695
return cls
621696

622697

623-
def trace_method(method: _F) -> _F:
698+
def trace_method(method: _F, name: Optional[str] = None) -> _F:
624699
"""Trace this method.
625700
626701
A span will be opened when this method is called and closed when it returns.
627702
"""
628-
return _trace_callable(method, "method")
703+
return _trace_callable(method, "method", name=name)
629704

630705

631-
def trace_function(function: _F) -> _F:
706+
def trace_function(function: _F, name: Optional[str] = None) -> _F:
632707
"""Trace this function.
633708
634709
A span will be opened when this function is called and closed when it returns.
635710
"""
636-
return _trace_callable(function, "function")
711+
return _trace_callable(function, "function", name=name)
637712

638713

639-
def _trace_callable(callable: _F, qualifier: str) -> _F:
714+
def _trace_callable(callable: _F, qualifier: str, name: Optional[str] = None) -> _F:
640715
dev_logger.info(f"instrumenting {callable}")
641716

642717
# sig = inspect.signature(callable)
643718
@functools.wraps(callable)
644719
def wrapped_function(*args, **kwargs): # type: ignore
645-
name = getattr(callable, "__qualname__", getattr(callable, "__name__", str(callable)))
646-
with _span(f"{qualifier} call: {name}"): # type: ignore
720+
name_ = name or getattr(
721+
callable, "__qualname__", getattr(callable, "__name__", str(callable))
722+
)
723+
with _span(f"{qualifier} call: {name_}"): # type: ignore
647724
return callable(*args, **kwargs) # type: ignore
648725

649726
# wrapped_function.__signature__ = sig

lib/charms/tempo_k8s/v2/tracing.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ def __init__(self, *args):
107107

108108
# Increment this PATCH version before using `charmcraft publish-lib` or reset
109109
# to 0 if you are raising the major API version
110-
LIBPATCH = 7
110+
LIBPATCH = 8
111111

112112
PYDEPS = ["pydantic"]
113113

@@ -116,14 +116,13 @@ def __init__(self, *args):
116116
DEFAULT_RELATION_NAME = "tracing"
117117
RELATION_INTERFACE_NAME = "tracing"
118118

119+
# Supported list rationale https://github.com/canonical/tempo-coordinator-k8s-operator/issues/8
119120
ReceiverProtocol = Literal[
120121
"zipkin",
121-
"kafka",
122-
"opencensus",
123-
"tempo_http",
124-
"tempo_grpc",
125122
"otlp_grpc",
126123
"otlp_http",
124+
"jaeger_grpc",
125+
"jaeger_thrift_http",
127126
]
128127

129128
RawReceiver = Tuple[ReceiverProtocol, str]
@@ -141,14 +140,12 @@ class TransportProtocolType(str, enum.Enum):
141140
grpc = "grpc"
142141

143142

144-
receiver_protocol_to_transport_protocol = {
143+
receiver_protocol_to_transport_protocol: Dict[ReceiverProtocol, TransportProtocolType] = {
145144
"zipkin": TransportProtocolType.http,
146-
"kafka": TransportProtocolType.http,
147-
"opencensus": TransportProtocolType.http,
148-
"tempo_http": TransportProtocolType.http,
149-
"tempo_grpc": TransportProtocolType.grpc,
150145
"otlp_grpc": TransportProtocolType.grpc,
151146
"otlp_http": TransportProtocolType.http,
147+
"jaeger_thrift_http": TransportProtocolType.http,
148+
"jaeger_grpc": TransportProtocolType.grpc,
152149
}
153150
"""A mapping between telemetry protocols and their corresponding transport protocol.
154151
"""

0 commit comments

Comments
 (0)