diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..5ce31d9 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,32 @@ +Applicable spec: + +### Overview + + + +### Rationale + + + +### Juju Events Changes + + + +### Module Changes + + + +### Library Changes + + + +### Checklist + +- [ ] The [charm style guide](https://juju.is/docs/sdk/styleguide) was applied +- [ ] The [contributing guide](https://github.com/canonical/is-charms-contributing-guide) was applied +- [ ] The changes are compliant with [ISD054 - Managing Charm Complexity](https://discourse.charmhub.io/t/specification-isd014-managing-charm-complexity/11619) +- [ ] The documentation is generated using `src-docs` +- [ ] The documentation for charmhub is updated +- [ ] The PR is tagged with appropriate label (`urgent`, `trivial`, `complex`) + + diff --git a/actions.yaml b/actions.yaml index 45d0228..9e9140f 100644 --- a/actions.yaml +++ b/actions.yaml @@ -1,4 +1,4 @@ -# Copyright 2023 Canonical Ltd. +# Copyright 2024 Canonical Ltd. # See LICENSE file for licensing details. report-visits-by-ip: diff --git a/charmcraft.yaml b/charmcraft.yaml index 9127512..4c562bf 100644 --- a/charmcraft.yaml +++ b/charmcraft.yaml @@ -1,4 +1,4 @@ -# Copyright 2023 Canonical Ltd. +# Copyright 2024 Canonical Ltd. # See LICENSE file for licensing details. type: charm diff --git a/config.yaml b/config.yaml index 424d33d..60e1e83 100644 --- a/config.yaml +++ b/config.yaml @@ -1,4 +1,4 @@ -# Copyright 2023 Canonical Ltd. +# Copyright 2024 Canonical Ltd. # See LICENSE file for licensing details. options: diff --git a/content-cache_rock/entrypoint.sh b/content-cache_rock/entrypoint.sh index c9f92b4..1bff63b 100755 --- a/content-cache_rock/entrypoint.sh +++ b/content-cache_rock/entrypoint.sh @@ -1,6 +1,6 @@ #!/bin/sh -# Copyright 2023 Canonical Ltd. +# Copyright 2024 Canonical Ltd. # See LICENSE file for licensing details. set -eu diff --git a/content-cache_rock/rockcraft.yaml b/content-cache_rock/rockcraft.yaml index 83be8e0..66fce64 100644 --- a/content-cache_rock/rockcraft.yaml +++ b/content-cache_rock/rockcraft.yaml @@ -1,4 +1,4 @@ -# Copyright 2023 Canonical Ltd. +# Copyright 2024 Canonical Ltd. # See LICENSE file for licensing details. name: content-cache summary: Content-cache-k8s ROCK image. diff --git a/docs/how-to/contribute.md b/docs/how-to/contribute.md index 31861d2..79e0196 100644 --- a/docs/how-to/contribute.md +++ b/docs/how-to/contribute.md @@ -56,6 +56,15 @@ Note that the [content-cache-image](content-cache.Dockerfile) image needs to be * `tox -e integration`: Runs the integration tests. +### Generating src docs for every commit + +Run the following command: + +```bash +echo -e "tox -e src-docs\ngit add src-docs\n" >> .git/hooks/pre-commit +chmod +x .git/hooks/pre-commit +``` + ## Build charm Build the charm in this git repository using: diff --git a/generate-src-docs.sh b/generate-src-docs.sh new file mode 100644 index 0000000..d13066a --- /dev/null +++ b/generate-src-docs.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +# Copyright 2024 Canonical Ltd. +# See LICENSE file for licensing details. + +lazydocs --no-watermark --output-path src-docs src/* diff --git a/lib/charms/loki_k8s/v0/loki_push_api.py b/lib/charms/loki_k8s/v0/loki_push_api.py index 9f9372d..0df057c 100644 --- a/lib/charms/loki_k8s/v0/loki_push_api.py +++ b/lib/charms/loki_k8s/v0/loki_push_api.py @@ -12,9 +12,9 @@ implement the provider side of the `loki_push_api` relation interface. For instance, a Loki charm. The provider side of the relation represents the server side, to which logs are being pushed. -- `LokiPushApiConsumer`: This object is meant to be used by any Charmed Operator that needs to -send log to Loki by implementing the consumer side of the `loki_push_api` relation interface. -For instance, a Promtail or Grafana agent charm which needs to send logs to Loki. +- `LokiPushApiConsumer`: Used to obtain the loki api endpoint. This is useful for configuring + applications such as pebble, or charmed operators of workloads such as grafana-agent or promtail, + that can communicate with loki directly. - `LogProxyConsumer`: This object can be used by any Charmed Operator which needs to send telemetry, such as logs, to Loki through a Log Proxy by implementing the consumer side of the @@ -480,7 +480,7 @@ def _alert_rules_error(self, event): # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 22 +LIBPATCH = 24 logger = logging.getLogger(__name__) @@ -604,7 +604,9 @@ def _validate_relation_by_interface_and_direction( actual_relation_interface = relation.interface_name if actual_relation_interface != expected_relation_interface: raise RelationInterfaceMismatchError( - relation_name, expected_relation_interface, actual_relation_interface + relation_name, + expected_relation_interface, + actual_relation_interface, # pyright: ignore ) if expected_relation_role == RelationRole.provides: @@ -866,20 +868,20 @@ def _from_dir(self, dir_path: Path, recursive: bool) -> List[dict]: return alert_groups - def add_path(self, path: str, *, recursive: bool = False): + def add_path(self, path_str: str, *, recursive: bool = False): """Add rules from a dir path. All rules from files are aggregated into a data structure representing a single rule file. All group names are augmented with juju topology. Args: - path: either a rules file or a dir of rules files. + path_str: either a rules file or a dir of rules files. recursive: whether to read files recursively or not (no impact if `path` is a file). Raises: InvalidAlertRulePathError: if the provided path is invalid. """ - path = Path(path) # type: Path + path = Path(path_str) # type: Path if path.is_dir(): self.alert_groups.extend(self._from_dir(path, recursive)) elif path.is_file(): @@ -992,6 +994,8 @@ def __init__(self, handle, relation, relation_id, app=None, unit=None): def snapshot(self) -> Dict: """Save event information.""" + if not self.relation: + return {} snapshot = {"relation_name": self.relation.name, "relation_id": self.relation.id} if self.app: snapshot["app_name"] = self.app.name @@ -1052,7 +1056,7 @@ class LokiPushApiEvents(ObjectEvents): class LokiPushApiProvider(Object): """A LokiPushApiProvider class.""" - on = LokiPushApiEvents() + on = LokiPushApiEvents() # pyright: ignore def __init__( self, @@ -1146,11 +1150,11 @@ def _on_logging_relation_changed(self, event: HookEvent): event: a `CharmEvent` in response to which the consumer charm must update its relation data. """ - should_update = self._process_logging_relation_changed(event.relation) + should_update = self._process_logging_relation_changed(event.relation) # pyright: ignore if should_update: self.on.loki_push_api_alert_rules_changed.emit( - relation=event.relation, - relation_id=event.relation.id, + relation=event.relation, # pyright: ignore + relation_id=event.relation.id, # pyright: ignore app=self._charm.app, unit=self._charm.unit, ) @@ -1517,7 +1521,7 @@ def loki_endpoints(self) -> List[dict]: class LokiPushApiConsumer(ConsumerBase): """Loki Consumer class.""" - on = LokiPushApiEvents() + on = LokiPushApiEvents() # pyright: ignore def __init__( self, @@ -1760,7 +1764,7 @@ class LogProxyConsumer(ConsumerBase): role. """ - on = LogProxyEvents() + on = LogProxyEvents() # pyright: ignore def __init__( self, @@ -1885,7 +1889,7 @@ def _on_relation_departed(self, _: RelationEvent) -> None: self._container.stop(WORKLOAD_SERVICE_NAME) self.on.log_proxy_endpoint_departed.emit() - def _get_container(self, container_name: str = "") -> Container: + def _get_container(self, container_name: str = "") -> Container: # pyright: ignore """Gets a single container by name or using the only container running in the Pod. If there is more than one container in the Pod a `PromtailDigestError` is emitted. @@ -1959,7 +1963,9 @@ def _add_pebble_layer(self, workload_binary_path: str) -> None: } }, } - self._container.add_layer(self._container_name, pebble_layer, combine=True) + self._container.add_layer( + self._container_name, pebble_layer, combine=True # pyright: ignore + ) def _create_directories(self) -> None: """Creates the directories for Promtail binary and config file.""" @@ -1996,7 +2002,11 @@ def _push_binary_to_workload(self, binary_path: str, workload_binary_path: str) """ with open(binary_path, "rb") as f: self._container.push( - workload_binary_path, f, permissions=0o755, encoding=None, make_dirs=True + workload_binary_path, + f, + permissions=0o755, + encoding=None, # pyright: ignore + make_dirs=True, ) logger.debug("The promtail binary file has been pushed to the workload container.") diff --git a/lib/charms/nginx_ingress_integrator/v0/ingress.py b/lib/charms/nginx_ingress_integrator/v0/ingress.py index 08dfe45..84857e7 100644 --- a/lib/charms/nginx_ingress_integrator/v0/ingress.py +++ b/lib/charms/nginx_ingress_integrator/v0/ingress.py @@ -1,4 +1,4 @@ -# Copyright 2023 Canonical Ltd. +# Copyright 2024 Canonical Ltd. # Licensed under the Apache2.0. See LICENSE file in charm source for details. """Library for the ingress relation. diff --git a/lib/charms/prometheus_k8s/v0/prometheus_scrape.py b/lib/charms/prometheus_k8s/v0/prometheus_scrape.py index e4297aa..665af88 100644 --- a/lib/charms/prometheus_k8s/v0/prometheus_scrape.py +++ b/lib/charms/prometheus_k8s/v0/prometheus_scrape.py @@ -362,7 +362,7 @@ def _on_scrape_targets_changed(self, event): # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 42 +LIBPATCH = 44 PYDEPS = ["cosl"] @@ -386,6 +386,7 @@ def _on_scrape_targets_changed(self, event): "basic_auth", "tls_config", "authorization", + "params", } DEFAULT_JOB = { "metrics_path": "/metrics", @@ -764,7 +765,7 @@ def _validate_relation_by_interface_and_direction( actual_relation_interface = relation.interface_name if actual_relation_interface != expected_relation_interface: raise RelationInterfaceMismatchError( - relation_name, expected_relation_interface, actual_relation_interface + relation_name, expected_relation_interface, actual_relation_interface or "None" ) if expected_relation_role == RelationRole.provides: @@ -857,7 +858,7 @@ class MonitoringEvents(ObjectEvents): class MetricsEndpointConsumer(Object): """A Prometheus based Monitoring service.""" - on = MonitoringEvents() + on = MonitoringEvents() # pyright: ignore def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME): """A Prometheus based Monitoring service. @@ -1014,7 +1015,6 @@ def alerts(self) -> dict: try: scrape_metadata = json.loads(relation.data[relation.app]["scrape_metadata"]) identifier = JujuTopology.from_dict(scrape_metadata).identifier - alerts[identifier] = self._tool.apply_label_matchers(alert_rules) # type: ignore except KeyError as e: logger.debug( @@ -1029,6 +1029,10 @@ def alerts(self) -> dict: ) continue + # We need to append the relation info to the identifier. This is to allow for cases for there are two + # relations which eventually scrape the same application. Issue #551. + identifier = f"{identifier}_{relation.name}_{relation.id}" + alerts[identifier] = alert_rules _, errmsg = self._tool.validate_alert_rules(alert_rules) @@ -1294,7 +1298,7 @@ def _resolve_dir_against_charm_path(charm: CharmBase, *path_elements: str) -> st class MetricsEndpointProvider(Object): """A metrics endpoint for Prometheus.""" - on = MetricsEndpointProviderEvents() + on = MetricsEndpointProviderEvents() # pyright: ignore def __init__( self, @@ -1836,14 +1840,16 @@ def _set_prometheus_data(self, event): return jobs = [] + _type_convert_stored( - self._stored.jobs + self._stored.jobs # pyright: ignore ) # list of scrape jobs, one per relation for relation in self.model.relations[self._target_relation]: targets = self._get_targets(relation) if targets and relation.app: jobs.append(self._static_scrape_job(targets, relation.app.name)) - groups = [] + _type_convert_stored(self._stored.alert_rules) # list of alert rule groups + groups = [] + _type_convert_stored( + self._stored.alert_rules # pyright: ignore + ) # list of alert rule groups for relation in self.model.relations[self._alert_rules_relation]: unit_rules = self._get_alert_rules(relation) if unit_rules and relation.app: @@ -1895,7 +1901,7 @@ def set_target_job_data(self, targets: dict, app_name: str, **kwargs) -> None: jobs.append(updated_job) relation.data[self._charm.app]["scrape_jobs"] = json.dumps(jobs) - if not _type_convert_stored(self._stored.jobs) == jobs: + if not _type_convert_stored(self._stored.jobs) == jobs: # pyright: ignore self._stored.jobs = jobs def _on_prometheus_targets_departed(self, event): @@ -1947,7 +1953,7 @@ def remove_prometheus_jobs(self, job_name: str, unit_name: Optional[str] = ""): relation.data[self._charm.app]["scrape_jobs"] = json.dumps(jobs) - if not _type_convert_stored(self._stored.jobs) == jobs: + if not _type_convert_stored(self._stored.jobs) == jobs: # pyright: ignore self._stored.jobs = jobs def _job_name(self, appname) -> str: @@ -2126,7 +2132,7 @@ def set_alert_rule_data(self, name: str, unit_rules: dict, label_rules: bool = T groups.append(updated_group) relation.data[self._charm.app]["alert_rules"] = json.dumps({"groups": groups}) - if not _type_convert_stored(self._stored.alert_rules) == groups: + if not _type_convert_stored(self._stored.alert_rules) == groups: # pyright: ignore self._stored.alert_rules = groups def _on_alert_rules_departed(self, event): @@ -2176,7 +2182,7 @@ def remove_alert_rules(self, group_name: str, unit_name: str) -> None: json.dumps({"groups": groups}) if groups else "{}" ) - if not _type_convert_stored(self._stored.alert_rules) == groups: + if not _type_convert_stored(self._stored.alert_rules) == groups: # pyright: ignore self._stored.alert_rules = groups def _get_alert_rules(self, relation) -> dict: diff --git a/metadata.yaml b/metadata.yaml index 1715f46..6b9e6cf 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -1,4 +1,4 @@ -# Copyright 2023 Canonical Ltd. +# Copyright 2024 Canonical Ltd. # See LICENSE file for licensing details. name: content-cache-k8s diff --git a/pyproject.toml b/pyproject.toml index 91ace93..65149b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,4 +1,4 @@ -# Copyright 2023 Canonical Ltd. +# Copyright 2024 Canonical Ltd. # See LICENSE file for licensing details. [tool.bandit] diff --git a/requirements.txt b/requirements.txt index 408016c..9684eab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ +cosl ops>=1.2.0 tabulate diff --git a/src-docs/charm.py.md b/src-docs/charm.py.md new file mode 100644 index 0000000..e7cac91 --- /dev/null +++ b/src-docs/charm.py.md @@ -0,0 +1,98 @@ + + + + +# module `charm.py` +Charm for Content-cache on Kubernetes. + +**Global Variables** +--------------- +- **REQUIRED_INGRESS_RELATION_FIELDS** +- **CACHE_PATH** +- **CONTAINER_NAME** +- **EXPORTER_CONTAINER_NAME** +- **CONTAINER_PORT** +- **REQUIRED_JUJU_CONFIGS** + + +--- + +## class `ContentCacheCharm` +Charm the service. + +Attrs: on: Ingress Charm Events ERROR_LOG_PATH: NGINX error log ACCESS_LOG_PATH: NGINX access log _metrics_endpoint: Provider of metrics for Prometheus charm _logging: Requirer of logs for Loki charm _grafana_dashboards: Dashboard Provider for Grafana charm ingress_proxy_provides: Ingress proxy provider ingress: Ingress requirer unit: Charm's designated juju unit model: Charm's designated juju model + + + +### function `__init__` + +```python +__init__(*args) +``` + +Init function for the charm. + + + +**Args:** + + - `args`: Variable list of positional arguments passed to the parent constructor. + + +--- + +#### property app + +Application that this unit is part of. + +--- + +#### property charm_dir + +Root directory of the charm as it is running. + +--- + +#### property config + +A mapping containing the charm's config and current values. + +--- + +#### property meta + +Metadata of this charm. + +--- + +#### property model + +Shortcut for more simple access the model. + +--- + +#### property unit + +Unit that this execution is responsible for. + + + +--- + + + +### function `configure_workload_container` + +```python +configure_workload_container(event: ConfigChangedEvent) → None +``` + +Configure/set up workload container inside pod. + + + +**Args:** + + - `event`: config-changed event. + + diff --git a/src-docs/file_reader.py.md b/src-docs/file_reader.py.md new file mode 100644 index 0000000..87566b9 --- /dev/null +++ b/src-docs/file_reader.py.md @@ -0,0 +1,32 @@ + + + + +# module `file_reader.py` +Short module for file reverse reading. + + +--- + + + +## function `readlines_reverse` + +```python +readlines_reverse(qfile) → Generator[int, NoneType, NoneType] +``` + +Read the lines of a file in reverse order in a lazy way. + + + +**Args:** + + - `qfile`: File in StringIO format. + + + +**Yields:** + A row from the read file. + + diff --git a/src/charm.py b/src/charm.py index ab7fef8..7ed86bf 100755 --- a/src/charm.py +++ b/src/charm.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2023 Canonical Ltd. +# Copyright 2024 Canonical Ltd. # See LICENSE file for licensing details. """Charm for Content-cache on Kubernetes.""" diff --git a/src/file_reader.py b/src/file_reader.py index dc6aa2f..efef6fc 100644 --- a/src/file_reader.py +++ b/src/file_reader.py @@ -1,6 +1,6 @@ """Short module for file reverse reading.""" -# Copyright 2023 Canonical Ltd. +# Copyright 2024 Canonical Ltd. # See LICENSE file for licensing details. import os diff --git a/tests/conftest.py b/tests/conftest.py index 5ef2d83..dd75ad3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,4 @@ -# Copyright 2023 Canonical Ltd. +# Copyright 2024 Canonical Ltd. # See LICENSE file for licensing details. """General configuration module for tests.""" diff --git a/tests/integration/any_charm.py b/tests/integration/any_charm.py index 441d219..eae765a 100644 --- a/tests/integration/any_charm.py +++ b/tests/integration/any_charm.py @@ -1,4 +1,4 @@ -# Copyright 2023 Canonical Ltd. +# Copyright 2024 Canonical Ltd. # See LICENSE file for licensing details. """This code snippet is used to be loaded into any-charm which is used for integration tests.""" diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 5e35bb5..5108e22 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,4 +1,4 @@ -# Copyright 2023 Canonical Ltd. +# Copyright 2024 Canonical Ltd. # See LICENSE file for licensing details. """General configuration module for integration tests.""" diff --git a/tests/integration/test_core.py b/tests/integration/test_core.py index 07ffeab..cdb1f46 100644 --- a/tests/integration/test_core.py +++ b/tests/integration/test_core.py @@ -1,4 +1,4 @@ -# Copyright 2023 Canonical Ltd. +# Copyright 2024 Canonical Ltd. # See LICENSE file for licensing details. """Integration test module.""" diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index 42d8828..ea6e138 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -1,4 +1,4 @@ -# Copyright 2023 Canonical Ltd. +# Copyright 2024 Canonical Ltd. # See LICENSE file for licensing details. import copy import io diff --git a/tox.ini b/tox.ini index bb5d90c..a379122 100644 --- a/tox.ini +++ b/tox.ini @@ -1,4 +1,4 @@ -# Copyright 2023 Canonical Ltd. +# Copyright 2024 Canonical Ltd. # See LICENSE file for licensing details. [tox] @@ -72,6 +72,19 @@ commands = -m pytest --ignore={[vars]tst_path}integration -v --tb native -s {posargs} coverage report +[testenv:src-docs] +allowlist_externals=sh +setenv = + PYTHONPATH = {toxinidir}:{toxinidir}/lib:{[vars]src_path} +description = Generate documentation for src +deps = + cosl + lazydocs + -r{toxinidir}/requirements.txt +commands = + ; can't run lazydocs directly due to needing to run it on src/* which produces an invocation error in tox + sh generate-src-docs.sh + [testenv:coverage-report] description = Create test coverage report deps = diff --git a/trivy.yaml b/trivy.yaml index 11603b1..2062a5c 100644 --- a/trivy.yaml +++ b/trivy.yaml @@ -1,4 +1,4 @@ -# Copyright 2023 Canonical Ltd. +# Copyright 2024 Canonical Ltd. # See LICENSE file for licensing details. exit-code: 0 timeout: 20m