paas-charm 2.0.dev1__tar.gz → 2.0.dev3__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.
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/PKG-INFO +4 -1
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/pyproject.toml +4 -1
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/_gunicorn/webserver.py +2 -26
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/_gunicorn/workload_config.py +0 -6
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/app.py +15 -4
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/charm.py +140 -68
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/charm_state.py +9 -1
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/exceptions.py +9 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/expressjs/charm.py +0 -1
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/fastapi/charm.py +0 -1
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/go/charm.py +0 -1
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/oauth.py +15 -4
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/observability.py +7 -42
- paas_charm-2.0.dev3/src/paas_charm/relations.py +213 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/springboot/charm.py +1 -2
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/tracing.py +4 -2
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/utils.py +0 -19
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm.egg-info/PKG-INFO +4 -1
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm.egg-info/SOURCES.txt +1 -9
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm.egg-info/requires.txt +3 -0
- paas_charm-2.0.dev3/src/paas_charm.egg-info/top_level.txt +1 -0
- paas_charm-2.0.dev1/src/paas_app_charmer/__init__.py +0 -13
- paas_charm-2.0.dev1/src/paas_app_charmer/django/__init__.py +0 -13
- paas_charm-2.0.dev1/src/paas_app_charmer/django/charm.py +0 -15
- paas_charm-2.0.dev1/src/paas_app_charmer/fastapi/__init__.py +0 -13
- paas_charm-2.0.dev1/src/paas_app_charmer/fastapi/charm.py +0 -16
- paas_charm-2.0.dev1/src/paas_app_charmer/flask/__init__.py +0 -13
- paas_charm-2.0.dev1/src/paas_app_charmer/flask/charm.py +0 -15
- paas_charm-2.0.dev1/src/paas_app_charmer/go/__init__.py +0 -13
- paas_charm-2.0.dev1/src/paas_app_charmer/go/charm.py +0 -15
- paas_charm-2.0.dev1/src/paas_charm.egg-info/top_level.txt +0 -2
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/README.md +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/setup.cfg +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/__init__.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/_gunicorn/__init__.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/_gunicorn/charm.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/_gunicorn/wsgi_app.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/charm_utils.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/database_migration.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/databases.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/django/__init__.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/django/charm.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/django/cos/grafana_dashboards/django.json +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/django/cos/loki_alert_rules/.gitkeep +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/django/cos/prometheus_alert_rules/django.rule +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/expressjs/__init__.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/expressjs/cos/grafana_dashboards/expressjs.json +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/expressjs/cos/loki_alert_rules/.gitkeep +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/expressjs/cos/prometheus_alert_rules/.gitkeep +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/fastapi/__init__.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/fastapi/app.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/fastapi/cos/grafana_dashboards/fastapi.json +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/fastapi/cos/loki_alert_rules/.gitkeep +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/fastapi/cos/prometheus_alert_rules/.gitkeep +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/flask/__init__.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/flask/charm.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/flask/cos/grafana_dashboards/flask.json +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/flask/cos/loki_alert_rules/.gitkeep +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/flask/cos/prometheus_alert_rules/flask.rule +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/framework.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/go/__init__.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/go/cos/grafana_dashboards/go.json +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/go/cos/loki_alert_rules/.gitkeep +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/go/cos/prometheus_alert_rules/.gitkeep +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/http_proxy.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/paas_config.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/peers.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/rabbitmq.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/s3.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/saml.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/secret_key.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/springboot/__init__.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/springboot/cos/grafana_dashboards/spring_boot.json +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/springboot/cos/loki_alert_rules/.gitkeep +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/springboot/cos/prometheus_alert_rules/.gitkeep +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/templates/_gunicorn_json_logging.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/templates/fastapi/uvicorn-log-config.json +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/templates/fastapi/uvicorn_log_handler.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/templates/gunicorn.conf.py.j2 +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm/valkey.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev3}/src/paas_charm.egg-info/dependency_links.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: paas-charm
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.dev3
|
|
4
4
|
Summary: Companion library for 12-factor app support in Charmcraft & Rockcraft.
|
|
5
5
|
Author-email: Canonical IS DevOps team <is-devops-team@canonical.com>
|
|
6
6
|
Project-URL: Repository, https://github.com/canonical/paas-charm
|
|
@@ -11,6 +11,9 @@ Classifier: Operating System :: OS Independent
|
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
12
|
Requires-Python: >=3.12
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: charmlibs-interfaces-tracing==1.0.0
|
|
15
|
+
Requires-Dist: charmlibs-interfaces-oauth==1.1.0
|
|
16
|
+
Requires-Dist: charmlibs-interfaces-openfga==1.0.0
|
|
14
17
|
Requires-Dist: cosl==1.10.3
|
|
15
18
|
Requires-Dist: dpcharmlibs-interfaces==1.2.0
|
|
16
19
|
Requires-Dist: Jinja2==3.1.6
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# See LICENSE file for licensing details.
|
|
3
3
|
[project]
|
|
4
4
|
name = "paas-charm"
|
|
5
|
-
version = "2.0.
|
|
5
|
+
version = "2.0.dev3"
|
|
6
6
|
description = "Companion library for 12-factor app support in Charmcraft & Rockcraft."
|
|
7
7
|
readme = "README.md"
|
|
8
8
|
authors = [
|
|
@@ -17,6 +17,9 @@ classifiers = [
|
|
|
17
17
|
"Programming Language :: Python :: 3",
|
|
18
18
|
]
|
|
19
19
|
dependencies = [
|
|
20
|
+
"charmlibs-interfaces-tracing==1.0.0",
|
|
21
|
+
"charmlibs-interfaces-oauth==1.1.0",
|
|
22
|
+
"charmlibs-interfaces-openfga==1.0.0",
|
|
20
23
|
"cosl==1.10.3",
|
|
21
24
|
"dpcharmlibs-interfaces==1.2.0",
|
|
22
25
|
"Jinja2==3.1.6",
|
|
@@ -17,14 +17,11 @@ import ops
|
|
|
17
17
|
from ops.pebble import ExecError, PathError
|
|
18
18
|
|
|
19
19
|
from paas_charm._gunicorn.workload_config import (
|
|
20
|
-
APPLICATION_ERROR_LOG_FILE,
|
|
21
|
-
APPLICATION_LOG_FILE,
|
|
22
20
|
STATSD_HOST,
|
|
23
21
|
)
|
|
24
22
|
from paas_charm.app import WorkloadConfig
|
|
25
23
|
from paas_charm.exceptions import CharmConfigInvalidError
|
|
26
24
|
from paas_charm.paas_config import LoggingFormat
|
|
27
|
-
from paas_charm.utils import enable_pebble_log_forwarding
|
|
28
25
|
|
|
29
26
|
logger = logging.getLogger(__name__)
|
|
30
27
|
|
|
@@ -153,21 +150,14 @@ class GunicornWebserver: # pylint: disable=too-few-public-methods
|
|
|
153
150
|
if isinstance(setting_value, (int, str))
|
|
154
151
|
else int(setting_value.total_seconds())
|
|
155
152
|
)
|
|
156
|
-
if enable_pebble_log_forwarding():
|
|
157
|
-
access_log = "-"
|
|
158
|
-
error_log = "-"
|
|
159
|
-
else:
|
|
160
|
-
access_log = str(APPLICATION_LOG_FILE)
|
|
161
|
-
error_log = str(APPLICATION_ERROR_LOG_FILE)
|
|
162
|
-
|
|
163
153
|
jinja_environment = jinja2.Environment(
|
|
164
154
|
loader=jinja2.PackageLoader("paas_charm", "templates"), autoescape=True
|
|
165
155
|
)
|
|
166
156
|
config = jinja_environment.get_template("gunicorn.conf.py.j2").render(
|
|
167
157
|
workload_port=self._workload_config.port,
|
|
168
158
|
workload_app_dir=str(self._workload_config.app_dir),
|
|
169
|
-
access_log=
|
|
170
|
-
error_log=
|
|
159
|
+
access_log="-",
|
|
160
|
+
error_log="-",
|
|
171
161
|
statsd_host=str(STATSD_HOST),
|
|
172
162
|
enable_tracing=self._workload_config.tracing_enabled,
|
|
173
163
|
enable_json_logging=self._workload_config.logging_format == LoggingFormat.JSON,
|
|
@@ -197,7 +187,6 @@ class GunicornWebserver: # pylint: disable=too-few-public-methods
|
|
|
197
187
|
Raises:
|
|
198
188
|
CharmConfigInvalidError: if the charm configuration is not valid.
|
|
199
189
|
"""
|
|
200
|
-
self._prepare_log_dir()
|
|
201
190
|
webserver_config_path = str(self._config_path)
|
|
202
191
|
try:
|
|
203
192
|
current_webserver_config = self._container.pull(webserver_config_path)
|
|
@@ -235,16 +224,3 @@ class GunicornWebserver: # pylint: disable=too-few-public-methods
|
|
|
235
224
|
if is_webserver_running:
|
|
236
225
|
logger.info("gunicorn config changed, reloading")
|
|
237
226
|
self._container.send_signal(self._reload_signal, self._workload_config.service_name)
|
|
238
|
-
|
|
239
|
-
def _prepare_log_dir(self) -> None:
|
|
240
|
-
"""Prepare access and error log directory for the application."""
|
|
241
|
-
container = self._container
|
|
242
|
-
for log in self._workload_config.log_files:
|
|
243
|
-
log_dir = str(log.parent.absolute())
|
|
244
|
-
if not container.exists(log_dir):
|
|
245
|
-
container.make_dir(
|
|
246
|
-
log_dir,
|
|
247
|
-
make_parents=True,
|
|
248
|
-
user=self._workload_config.user,
|
|
249
|
-
group=self._workload_config.group,
|
|
250
|
-
)
|
|
@@ -9,8 +9,6 @@ from paas_charm.app import WorkloadConfig
|
|
|
9
9
|
from paas_charm.paas_config import PaasConfig
|
|
10
10
|
|
|
11
11
|
STATSD_HOST = "localhost:9125"
|
|
12
|
-
APPLICATION_LOG_FILE = "/var/log/app/access.log"
|
|
13
|
-
APPLICATION_ERROR_LOG_FILE = "/var/log/app/error.log"
|
|
14
12
|
GUNICORN_CONFIG_DIR = "/var/lib/gunicorn"
|
|
15
13
|
|
|
16
14
|
|
|
@@ -50,10 +48,6 @@ def create_workload_config(
|
|
|
50
48
|
app_dir=app_dir,
|
|
51
49
|
state_dir=state_dir,
|
|
52
50
|
service_name=framework_name,
|
|
53
|
-
log_files=[
|
|
54
|
-
pathlib.Path(APPLICATION_LOG_FILE),
|
|
55
|
-
pathlib.Path(APPLICATION_ERROR_LOG_FILE),
|
|
56
|
-
],
|
|
57
51
|
metrics_path=metrics_path,
|
|
58
52
|
metrics_port=metrics_port,
|
|
59
53
|
unit_name=unit_name,
|
|
@@ -9,7 +9,7 @@ import logging
|
|
|
9
9
|
import pathlib
|
|
10
10
|
import urllib.parse
|
|
11
11
|
from dataclasses import dataclass
|
|
12
|
-
from typing import TYPE_CHECKING
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
13
|
|
|
14
14
|
import ops
|
|
15
15
|
from dpcharmlibs.interfaces import ValkeyResponseModel
|
|
@@ -21,7 +21,7 @@ from paas_charm.paas_config import LoggingFormat
|
|
|
21
21
|
logger = logging.getLogger(__name__)
|
|
22
22
|
|
|
23
23
|
if TYPE_CHECKING:
|
|
24
|
-
from
|
|
24
|
+
from charmlibs.interfaces.openfga import OpenfgaProviderAppData
|
|
25
25
|
from charms.smtp_integrator.v0.smtp import SmtpRelationData
|
|
26
26
|
|
|
27
27
|
from paas_charm.databases import PaaSDatabaseRelationData
|
|
@@ -52,7 +52,6 @@ class WorkloadConfig: # pylint: disable=too-many-instance-attributes
|
|
|
52
52
|
app_dir: the application directory in the application container.
|
|
53
53
|
state_dir: the directory in the application container to store states information.
|
|
54
54
|
service_name: the WSGI application pebble service name.
|
|
55
|
-
log_files: list of files to monitor.
|
|
56
55
|
metrics_path: path to scrape for metrics.
|
|
57
56
|
metrics_port: port on which the application exposes metrics.
|
|
58
57
|
unit_name: Name of the unit. Needed to know if schedulers should run here.
|
|
@@ -69,7 +68,6 @@ class WorkloadConfig: # pylint: disable=too-many-instance-attributes
|
|
|
69
68
|
app_dir: pathlib.Path
|
|
70
69
|
state_dir: pathlib.Path
|
|
71
70
|
service_name: str
|
|
72
|
-
log_files: List[pathlib.Path]
|
|
73
71
|
metrics_path: str | None = "/metrics"
|
|
74
72
|
metrics_port: int = 8080
|
|
75
73
|
unit_name: str
|
|
@@ -446,6 +444,7 @@ class App: # pylint: disable=too-many-instance-attributes
|
|
|
446
444
|
|
|
447
445
|
# 2024/04/25 - we're refactoring this method which will get rid of map_integrations_to_env
|
|
448
446
|
# wrapper function. Ignore too-complex error from flake8 for now.
|
|
447
|
+
# pylint: disable=too-many-branches
|
|
449
448
|
def gen_environment(self) -> dict[str, str]: # noqa: too-complex
|
|
450
449
|
"""Generate a environment dictionary from the charm configurations.
|
|
451
450
|
|
|
@@ -501,6 +500,7 @@ class App: # pylint: disable=too-many-instance-attributes
|
|
|
501
500
|
env[f"{prefix}PEER_FQDNS"] = self._charm_state.peer_fqdns
|
|
502
501
|
|
|
503
502
|
env.update(self._generate_integration_environments(prefix=self.integrations_prefix))
|
|
503
|
+
|
|
504
504
|
return env
|
|
505
505
|
|
|
506
506
|
def _generate_integration_environments(self, prefix: str = "") -> dict[str, str]:
|
|
@@ -531,6 +531,17 @@ class App: # pylint: disable=too-many-instance-attributes
|
|
|
531
531
|
relation_data=self._charm_state.integrations.oauth,
|
|
532
532
|
)
|
|
533
533
|
)
|
|
534
|
+
|
|
535
|
+
for relation in self._charm_state.custom_relations:
|
|
536
|
+
for key, value in (relation.gen_environment() or {}).items():
|
|
537
|
+
if key in env:
|
|
538
|
+
logger.warning(
|
|
539
|
+
"Custom relation %s overwrites environment variable %s",
|
|
540
|
+
relation.relation_name,
|
|
541
|
+
key,
|
|
542
|
+
)
|
|
543
|
+
env[key] = value
|
|
544
|
+
|
|
534
545
|
return {prefix + k: v for (k, v) in env.items()}
|
|
535
546
|
|
|
536
547
|
@property
|
|
@@ -9,8 +9,8 @@ import pathlib
|
|
|
9
9
|
import typing
|
|
10
10
|
|
|
11
11
|
import ops
|
|
12
|
+
from charmlibs.interfaces.openfga import OpenFGARequires
|
|
12
13
|
from charms.data_platform_libs.v0.data_interfaces import DatabaseRequiresEvent
|
|
13
|
-
from charms.openfga_k8s.v1.openfga import OpenFGARequires
|
|
14
14
|
from charms.smtp_integrator.v0.smtp import SmtpRequires
|
|
15
15
|
from charms.traefik_k8s.v2.ingress import IngressPerAppRequirer
|
|
16
16
|
from ops import RelationMeta
|
|
@@ -22,7 +22,11 @@ from paas_charm.charm_state import CharmState, IntegrationRequirers
|
|
|
22
22
|
from paas_charm.charm_utils import block_if_invalid_data
|
|
23
23
|
from paas_charm.database_migration import DatabaseMigration, DatabaseMigrationStatus
|
|
24
24
|
from paas_charm.databases import make_database_requirers
|
|
25
|
-
from paas_charm.exceptions import
|
|
25
|
+
from paas_charm.exceptions import (
|
|
26
|
+
CharmConfigInvalidError,
|
|
27
|
+
CustomRelationError,
|
|
28
|
+
RelationDataError,
|
|
29
|
+
)
|
|
26
30
|
from paas_charm.http_proxy import PaaSHttpProxyRequirer
|
|
27
31
|
from paas_charm.oauth import PaaSOAuthRequirer
|
|
28
32
|
from paas_charm.observability import Observability
|
|
@@ -33,6 +37,7 @@ from paas_charm.paas_config import (
|
|
|
33
37
|
)
|
|
34
38
|
from paas_charm.peers import Peers
|
|
35
39
|
from paas_charm.rabbitmq import RabbitMQRequires
|
|
40
|
+
from paas_charm.relations import Context, CustomRelation
|
|
36
41
|
from paas_charm.s3 import PaaSS3Requirer
|
|
37
42
|
from paas_charm.saml import PaaSSAMLRequirer
|
|
38
43
|
from paas_charm.secret_key import SecretKeyStorage
|
|
@@ -54,10 +59,12 @@ class PaasCharm(abc.ABC, ops.CharmBase): # pylint: disable=too-many-instance-at
|
|
|
54
59
|
Attrs:
|
|
55
60
|
framework_config_class: base class for the framework config.
|
|
56
61
|
paas_config_framework_fields: Mapping from framework fields to paas-config fields.
|
|
62
|
+
custom_relations: Classes describing charm relations.
|
|
57
63
|
"""
|
|
58
64
|
|
|
59
65
|
framework_config_class: type[BaseModel]
|
|
60
66
|
paas_config_framework_fields: dict[str, str] = {}
|
|
67
|
+
custom_relations: list[type[CustomRelation]] = []
|
|
61
68
|
|
|
62
69
|
@property
|
|
63
70
|
@abc.abstractmethod
|
|
@@ -128,7 +135,6 @@ class PaasCharm(abc.ABC, ops.CharmBase): # pylint: disable=too-many-instance-at
|
|
|
128
135
|
)
|
|
129
136
|
self._observability = Observability(
|
|
130
137
|
charm=self,
|
|
131
|
-
log_files=self._workload_config.log_files,
|
|
132
138
|
container_name=self._workload_config.container_name,
|
|
133
139
|
cos_dir=self.build_cos_dir(),
|
|
134
140
|
metrics_port=self._workload_config.metrics_port,
|
|
@@ -173,6 +179,8 @@ class PaasCharm(abc.ABC, ops.CharmBase): # pylint: disable=too-many-instance-at
|
|
|
173
179
|
self._reconcile_without_migrations,
|
|
174
180
|
)
|
|
175
181
|
|
|
182
|
+
self._custom_relations: list[CustomRelation] = self._init_custom_relations()
|
|
183
|
+
|
|
176
184
|
def _init_valkey(self, requires: dict[str, RelationMeta]) -> "ValkeyClientRequirer | None":
|
|
177
185
|
"""Initialize the Valkey relation if it is required.
|
|
178
186
|
|
|
@@ -299,21 +307,15 @@ class PaasCharm(abc.ABC, ops.CharmBase): # pylint: disable=too-many-instance-at
|
|
|
299
307
|
"""
|
|
300
308
|
_tracing = None
|
|
301
309
|
if "tracing" in requires and requires["tracing"].interface_name == "tracing":
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
self.
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
self.
|
|
310
|
-
|
|
311
|
-
)
|
|
312
|
-
except NameError:
|
|
313
|
-
logger.exception(
|
|
314
|
-
"Missing charm library, please run "
|
|
315
|
-
"`charmcraft fetch-lib charms.tempo_coordinator_k8s.v0.tracing`"
|
|
316
|
-
)
|
|
310
|
+
_tracing = PaaSTracingEndpointRequirer(
|
|
311
|
+
self, relation_name="tracing", protocols=["otlp_http"]
|
|
312
|
+
)
|
|
313
|
+
self.framework.observe(
|
|
314
|
+
_tracing.on.endpoint_changed, self._reconcile_without_migrations
|
|
315
|
+
)
|
|
316
|
+
self.framework.observe(
|
|
317
|
+
_tracing.on.endpoint_removed, self._reconcile_without_migrations
|
|
318
|
+
)
|
|
317
319
|
return _tracing
|
|
318
320
|
|
|
319
321
|
def _init_smtp(self, requires: dict[str, RelationMeta]) -> "SmtpRequires | None":
|
|
@@ -350,16 +352,10 @@ class PaasCharm(abc.ABC, ops.CharmBase): # pylint: disable=too-many-instance-at
|
|
|
350
352
|
"""
|
|
351
353
|
openfga = None
|
|
352
354
|
if "openfga" in requires and requires["openfga"].interface_name == "openfga":
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
self.
|
|
356
|
-
|
|
357
|
-
)
|
|
358
|
-
except NameError:
|
|
359
|
-
logger.exception(
|
|
360
|
-
"Missing charm library, please run "
|
|
361
|
-
"`charmcraft fetch-lib charms.openfga_k8s.v1.openfga`"
|
|
362
|
-
)
|
|
355
|
+
openfga = OpenFGARequires(self, self.app.name)
|
|
356
|
+
self.framework.observe(
|
|
357
|
+
openfga.on.openfga_store_created, self._reconcile_without_migrations
|
|
358
|
+
)
|
|
363
359
|
return openfga
|
|
364
360
|
|
|
365
361
|
def _init_oauth(self, requires: dict[str, RelationMeta]) -> "PaaSOAuthRequirer | None":
|
|
@@ -376,26 +372,62 @@ class PaasCharm(abc.ABC, ops.CharmBase): # pylint: disable=too-many-instance-at
|
|
|
376
372
|
if len(oauth_integrations) != 1:
|
|
377
373
|
return None
|
|
378
374
|
endpoint_name = oauth_integrations[0][0]
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
_oauth.on.oauth_info_changed, self._reconcile_without_migrations
|
|
388
|
-
)
|
|
389
|
-
self.framework.observe(
|
|
390
|
-
_oauth.on.oauth_info_removed, self._reconcile_without_migrations
|
|
391
|
-
)
|
|
392
|
-
except NameError:
|
|
393
|
-
logger.exception(
|
|
394
|
-
"Missing charm library, please run `charmcraft fetch-lib charms.hydra.v0.oauth`"
|
|
395
|
-
)
|
|
396
|
-
return None
|
|
375
|
+
_oauth = PaaSOAuthRequirer(
|
|
376
|
+
charm=self,
|
|
377
|
+
base_url=self._base_url,
|
|
378
|
+
relation_name=endpoint_name,
|
|
379
|
+
charm_config=self.config,
|
|
380
|
+
)
|
|
381
|
+
self.framework.observe(_oauth.on.oauth_info_changed, self._reconcile_without_migrations)
|
|
382
|
+
self.framework.observe(_oauth.on.oauth_info_removed, self._reconcile_without_migrations)
|
|
397
383
|
return _oauth
|
|
398
384
|
|
|
385
|
+
def _build_custom_relation_context(self) -> Context:
|
|
386
|
+
"""Build the read-only :class:`Context` injected into custom relations.
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
A :class:`Context` populated with the current charm configuration.
|
|
390
|
+
"""
|
|
391
|
+
return Context(
|
|
392
|
+
app_name=self.app.name,
|
|
393
|
+
framework_name=self._framework_name,
|
|
394
|
+
port=self._workload_config.port,
|
|
395
|
+
container_name=self._workload_config.container_name,
|
|
396
|
+
config=self._resolve_charm_config(),
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
def _init_custom_relations(self) -> list[CustomRelation]:
|
|
400
|
+
"""Instantiate and wire author-supplied custom relations.
|
|
401
|
+
|
|
402
|
+
Each class in :attr:`custom_relations` is validated against the charm
|
|
403
|
+
metadata, instantiated as an ``ops.Object`` child, injected with
|
|
404
|
+
:class:`Context` and the required flag (read from the metadata
|
|
405
|
+
``optional`` field), and finally set up with the reconcile callback.
|
|
406
|
+
|
|
407
|
+
Returns:
|
|
408
|
+
The list of instantiated custom relations.
|
|
409
|
+
"""
|
|
410
|
+
if not self.custom_relations:
|
|
411
|
+
return []
|
|
412
|
+
context = self._build_custom_relation_context()
|
|
413
|
+
requires: dict[str, RelationMeta] = self.framework.meta.requires
|
|
414
|
+
relations: list[CustomRelation] = []
|
|
415
|
+
for relation_class in self.custom_relations:
|
|
416
|
+
if not isinstance(relation_class, type) or not issubclass(
|
|
417
|
+
relation_class, CustomRelation
|
|
418
|
+
):
|
|
419
|
+
raise CustomRelationError(f"non-CustomRelation entry: {relation_class!r}")
|
|
420
|
+
relation_name = relation_class.relation_name
|
|
421
|
+
if relation_name not in requires:
|
|
422
|
+
raise CustomRelationError(f"Unused custom relation: {relation_class!r}")
|
|
423
|
+
instance = relation_class(self)
|
|
424
|
+
# Framework-injected private state; pylint: disable=protected-access
|
|
425
|
+
instance._context = context
|
|
426
|
+
instance._required = not requires[relation_name].optional
|
|
427
|
+
instance.setup(on_change=self._reconcile)
|
|
428
|
+
relations.append(instance)
|
|
429
|
+
return relations
|
|
430
|
+
|
|
399
431
|
def get_framework_config(self) -> BaseModel:
|
|
400
432
|
"""Return the framework related configurations.
|
|
401
433
|
|
|
@@ -407,14 +439,7 @@ class PaasCharm(abc.ABC, ops.CharmBase): # pylint: disable=too-many-instance-at
|
|
|
407
439
|
"""
|
|
408
440
|
# Will raise an AttributeError if it the attribute framework_config_class does not exist.
|
|
409
441
|
framework_config_class = self.framework_config_class
|
|
410
|
-
|
|
411
|
-
config = typing.cast(
|
|
412
|
-
dict,
|
|
413
|
-
{
|
|
414
|
-
k: v.get_content(refresh=True) if isinstance(v, ops.Secret) else v
|
|
415
|
-
for k, v in charm_config.items()
|
|
416
|
-
},
|
|
417
|
-
)
|
|
442
|
+
config = self._resolve_charm_config()
|
|
418
443
|
for framework_field, paas_config_field in self.paas_config_framework_fields.items():
|
|
419
444
|
if paas_config_field not in self._paas_config.model_fields_set:
|
|
420
445
|
continue
|
|
@@ -523,12 +548,7 @@ class PaasCharm(abc.ABC, ops.CharmBase): # pylint: disable=too-many-instance-at
|
|
|
523
548
|
self.update_app_and_unit_status(ops.WaitingStatus("Waiting for secret key creation"))
|
|
524
549
|
return False
|
|
525
550
|
|
|
526
|
-
|
|
527
|
-
if missing_integrations:
|
|
528
|
-
self._create_app().stop_all_services()
|
|
529
|
-
self._database_migration.set_status_to_pending()
|
|
530
|
-
logger.info(message := f"missing integrations: {', '.join(missing_integrations)}")
|
|
531
|
-
self.update_app_and_unit_status(ops.BlockedStatus(message))
|
|
551
|
+
if self._has_missing_relations(charm_state):
|
|
532
552
|
return False
|
|
533
553
|
|
|
534
554
|
if self._oauth and self._oauth.is_related():
|
|
@@ -549,6 +569,33 @@ class PaasCharm(abc.ABC, ops.CharmBase): # pylint: disable=too-many-instance-at
|
|
|
549
569
|
return False
|
|
550
570
|
return True
|
|
551
571
|
|
|
572
|
+
def _stop_all_and_set_blocked(self, message: str) -> None:
|
|
573
|
+
"""Stop services, mark database migrations as pending and set ``BlockedStatus``."""
|
|
574
|
+
self._create_app().stop_all_services()
|
|
575
|
+
self._database_migration.set_status_to_pending()
|
|
576
|
+
self.update_app_and_unit_status(ops.BlockedStatus(message))
|
|
577
|
+
|
|
578
|
+
def _has_missing_relations(self, charm_state: CharmState) -> bool:
|
|
579
|
+
"""Check if any relations are missing or not ready."""
|
|
580
|
+
missing_integrations = list(self._missing_required_integrations(charm_state))
|
|
581
|
+
if missing_integrations:
|
|
582
|
+
logger.info(message := f"missing integrations: {', '.join(missing_integrations)}")
|
|
583
|
+
self._stop_all_and_set_blocked(message)
|
|
584
|
+
return True
|
|
585
|
+
|
|
586
|
+
try:
|
|
587
|
+
missing_integrations = list(self._missing_custom_relations())
|
|
588
|
+
if missing_integrations:
|
|
589
|
+
logger.info(message := f"missing integrations: {', '.join(missing_integrations)}")
|
|
590
|
+
self._stop_all_and_set_blocked(message)
|
|
591
|
+
return True
|
|
592
|
+
except RelationDataError as e:
|
|
593
|
+
logger.error(message := f"RelationDataError: {e}")
|
|
594
|
+
self._stop_all_and_set_blocked(message)
|
|
595
|
+
return True
|
|
596
|
+
|
|
597
|
+
return False
|
|
598
|
+
|
|
552
599
|
def _missing_required_database_integrations(
|
|
553
600
|
self, requires: dict[str, RelationMeta], charm_state: CharmState
|
|
554
601
|
) -> typing.Generator:
|
|
@@ -621,6 +668,29 @@ class PaasCharm(abc.ABC, ops.CharmBase): # pylint: disable=too-many-instance-at
|
|
|
621
668
|
):
|
|
622
669
|
yield "valkey"
|
|
623
670
|
|
|
671
|
+
def _missing_custom_relations(self) -> typing.Generator:
|
|
672
|
+
"""Return custom relations that are not established and required.
|
|
673
|
+
|
|
674
|
+
For not established but required relations this method returns a list
|
|
675
|
+
of the relation names. If a relation is not ready due to some problems
|
|
676
|
+
with missing / invalid data and raises an exception, the exception
|
|
677
|
+
is propagated to the caller.
|
|
678
|
+
|
|
679
|
+
Raises:
|
|
680
|
+
RelationDataError: if relation is not ready due to missing or
|
|
681
|
+
invalid data.
|
|
682
|
+
""" # noqa: DCO051
|
|
683
|
+
for relation in self._custom_relations:
|
|
684
|
+
name = relation.relation_name
|
|
685
|
+
related = any(
|
|
686
|
+
model_relation.active for model_relation in self.model.relations.get(name, [])
|
|
687
|
+
)
|
|
688
|
+
if related:
|
|
689
|
+
# may raise RelationDataError or InvalidRelationDataError
|
|
690
|
+
relation.ensure_ready()
|
|
691
|
+
elif relation.required:
|
|
692
|
+
yield name
|
|
693
|
+
|
|
624
694
|
def _missing_required_integrations(
|
|
625
695
|
self, charm_state: CharmState
|
|
626
696
|
) -> typing.Generator: # noqa: C901
|
|
@@ -677,6 +747,14 @@ class PaasCharm(abc.ABC, ops.CharmBase): # pylint: disable=too-many-instance-at
|
|
|
677
747
|
"""
|
|
678
748
|
return self._create_app().gen_environment()
|
|
679
749
|
|
|
750
|
+
def _resolve_charm_config(self) -> dict[str, typing.Any]:
|
|
751
|
+
"""Resolve any secrets in the config."""
|
|
752
|
+
charm_config = {k: config_get_with_secret(self, k) for k in self.config}
|
|
753
|
+
return {
|
|
754
|
+
k: v.get_content(refresh=True) if isinstance(v, ops.Secret) else v
|
|
755
|
+
for k, v in charm_config.items()
|
|
756
|
+
}
|
|
757
|
+
|
|
680
758
|
def _create_charm_state(self) -> CharmState:
|
|
681
759
|
"""Create charm state.
|
|
682
760
|
|
|
@@ -685,14 +763,7 @@ class PaasCharm(abc.ABC, ops.CharmBase): # pylint: disable=too-many-instance-at
|
|
|
685
763
|
Returns:
|
|
686
764
|
New CharmState
|
|
687
765
|
"""
|
|
688
|
-
|
|
689
|
-
config = typing.cast(
|
|
690
|
-
dict,
|
|
691
|
-
{
|
|
692
|
-
k: v.get_content(refresh=True) if isinstance(v, ops.Secret) else v
|
|
693
|
-
for k, v in charm_config.items()
|
|
694
|
-
},
|
|
695
|
-
)
|
|
766
|
+
config = self._resolve_charm_config()
|
|
696
767
|
return CharmState.from_charm(
|
|
697
768
|
charm_dir=self.charm_dir,
|
|
698
769
|
config=config,
|
|
@@ -712,6 +783,7 @@ class PaasCharm(abc.ABC, ops.CharmBase): # pylint: disable=too-many-instance-at
|
|
|
712
783
|
oauth=self._oauth,
|
|
713
784
|
http_proxy=self._http_proxy,
|
|
714
785
|
),
|
|
786
|
+
custom_relations=self._custom_relations,
|
|
715
787
|
base_url=self._base_url,
|
|
716
788
|
)
|
|
717
789
|
|
|
@@ -17,12 +17,13 @@ from paas_charm.exceptions import (
|
|
|
17
17
|
RelationDataError,
|
|
18
18
|
)
|
|
19
19
|
from paas_charm.peers import Peers
|
|
20
|
+
from paas_charm.relations import CustomRelation
|
|
20
21
|
from paas_charm.secret_key import SecretKeyStorage
|
|
21
22
|
from paas_charm.utils import build_validation_error_message, config_metadata
|
|
22
23
|
|
|
23
24
|
# This is just for type checking, no need to cover this code.
|
|
24
25
|
if typing.TYPE_CHECKING: # pragma: nocover
|
|
25
|
-
from
|
|
26
|
+
from charmlibs.interfaces.openfga import OpenfgaProviderAppData, OpenFGARequires
|
|
26
27
|
from charms.smtp_integrator.v0.smtp import SmtpRelationData, SmtpRequires
|
|
27
28
|
from charms.squid_forward_proxy.v0.http_proxy import ProxyConfig
|
|
28
29
|
from dpcharmlibs.interfaces import ValkeyResponseModel
|
|
@@ -62,6 +63,7 @@ class CharmState: # pylint: disable=too-many-instance-attributes
|
|
|
62
63
|
peer_fqdns: str | None = None,
|
|
63
64
|
integrations: "IntegrationsState | None" = None,
|
|
64
65
|
base_url: str | None = None,
|
|
66
|
+
custom_relations: list[CustomRelation] | None = None,
|
|
65
67
|
):
|
|
66
68
|
"""Initialize a new instance of the CharmState class.
|
|
67
69
|
|
|
@@ -74,6 +76,7 @@ class CharmState: # pylint: disable=too-many-instance-attributes
|
|
|
74
76
|
peer_fqdns: The FQDN of units in the peer relation.
|
|
75
77
|
integrations: Information about the integrations.
|
|
76
78
|
base_url: Base URL for the service.
|
|
79
|
+
custom_relations: Custom relations.
|
|
77
80
|
"""
|
|
78
81
|
self.framework = framework
|
|
79
82
|
self._framework_config = framework_config if framework_config is not None else {}
|
|
@@ -83,6 +86,7 @@ class CharmState: # pylint: disable=too-many-instance-attributes
|
|
|
83
86
|
self.peer_fqdns = peer_fqdns
|
|
84
87
|
self.integrations = integrations or IntegrationsState()
|
|
85
88
|
self.base_url = base_url
|
|
89
|
+
self.custom_relations = custom_relations or []
|
|
86
90
|
|
|
87
91
|
@classmethod
|
|
88
92
|
def from_charm( # pylint: disable=too-many-arguments,too-many-locals
|
|
@@ -95,6 +99,7 @@ class CharmState: # pylint: disable=too-many-instance-attributes
|
|
|
95
99
|
secret_key: SecretKeyStorage,
|
|
96
100
|
peers: Peers,
|
|
97
101
|
integration_requirers: "IntegrationRequirers",
|
|
102
|
+
custom_relations: list[CustomRelation] | None = None,
|
|
98
103
|
base_url: str | None = None,
|
|
99
104
|
) -> "CharmState":
|
|
100
105
|
"""Initialize a new instance of the CharmState class from the associated charm.
|
|
@@ -107,6 +112,7 @@ class CharmState: # pylint: disable=too-many-instance-attributes
|
|
|
107
112
|
secret_key: The application secret key manager.
|
|
108
113
|
peers: The peer coordination helper.
|
|
109
114
|
integration_requirers: The collection of integration requirers.
|
|
115
|
+
custom_relations: Custom relations.
|
|
110
116
|
base_url: Base URL for the service.
|
|
111
117
|
|
|
112
118
|
Return:
|
|
@@ -210,6 +216,7 @@ class CharmState: # pylint: disable=too-many-instance-attributes
|
|
|
210
216
|
f"{exc.relation} relation data is either unavailable, invalid or not usable.",
|
|
211
217
|
relation=exc.relation,
|
|
212
218
|
) from exc
|
|
219
|
+
|
|
213
220
|
peer_fqdns = None
|
|
214
221
|
if peers.is_related and (peer_unit_fqdns := peers.get_peer_unit_fqdns()):
|
|
215
222
|
peer_fqdns = ",".join(peer_unit_fqdns)
|
|
@@ -225,6 +232,7 @@ class CharmState: # pylint: disable=too-many-instance-attributes
|
|
|
225
232
|
peer_fqdns=peer_fqdns,
|
|
226
233
|
integrations=integrations,
|
|
227
234
|
base_url=base_url,
|
|
235
|
+
custom_relations=custom_relations,
|
|
228
236
|
)
|
|
229
237
|
|
|
230
238
|
@property
|
|
@@ -68,3 +68,12 @@ class InvalidRelationDataError(RelationDataError):
|
|
|
68
68
|
|
|
69
69
|
class InvalidCustomCOSDirectoryError(Exception):
|
|
70
70
|
"""Raised when the custom COS directory is invalid."""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class CustomRelationError(Exception):
|
|
74
|
+
"""Raised when a custom relation class is invalid.
|
|
75
|
+
|
|
76
|
+
Raised when an entry in ``custom_relations`` is not a ``CustomRelation``
|
|
77
|
+
subclass, or when the relation class is not declared in the charm metadata
|
|
78
|
+
(unused custom relation).
|
|
79
|
+
"""
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import logging
|
|
7
7
|
|
|
8
8
|
import ops
|
|
9
|
-
from
|
|
9
|
+
from charmlibs.interfaces.oauth import ClientConfig, OAuthRequirer
|
|
10
10
|
from ops import ConfigData
|
|
11
11
|
from pydantic import BaseModel, ValidationError
|
|
12
12
|
|
|
@@ -95,6 +95,11 @@ class PaaSOAuthRequirer(OAuthRequirer):
|
|
|
95
95
|
if not self.is_client_created():
|
|
96
96
|
return None
|
|
97
97
|
prod_info = self.get_provider_info()
|
|
98
|
+
client_config = self._client_config
|
|
99
|
+
if prod_info is None or client_config is None:
|
|
100
|
+
raise InvalidOAuthRelationDataError(
|
|
101
|
+
"OAuth provider or client configuration is unavailable."
|
|
102
|
+
)
|
|
98
103
|
user_name_attribute = str(
|
|
99
104
|
self._charm_config.get(f"{self._relation_name}-user-name-attribute", "sub")
|
|
100
105
|
)
|
|
@@ -107,9 +112,9 @@ class PaaSOAuthRequirer(OAuthRequirer):
|
|
|
107
112
|
"token_endpoint": prod_info.token_endpoint,
|
|
108
113
|
"userinfo_endpoint": prod_info.userinfo_endpoint,
|
|
109
114
|
"jwks_endpoint": prod_info.jwks_endpoint,
|
|
110
|
-
"scopes":
|
|
115
|
+
"scopes": client_config.scope,
|
|
111
116
|
"provider_name": self._relation_name,
|
|
112
|
-
"redirect_uri":
|
|
117
|
+
"redirect_uri": client_config.redirect_uri,
|
|
113
118
|
"user_name_attribute": user_name_attribute,
|
|
114
119
|
}
|
|
115
120
|
)
|
|
@@ -160,7 +165,13 @@ class PaaSOAuthRequirer(OAuthRequirer):
|
|
|
160
165
|
def get_related_app_name(self) -> str:
|
|
161
166
|
"""Return the related Oauth provider charm's name.
|
|
162
167
|
|
|
168
|
+
Raises:
|
|
169
|
+
InvalidOAuthRelationDataError: If no OAuth provider is related.
|
|
170
|
+
|
|
163
171
|
Returns:
|
|
164
172
|
Name of the Oauth provider charm.
|
|
165
173
|
"""
|
|
166
|
-
|
|
174
|
+
relations = self.model.relations.get(self._relation_name)
|
|
175
|
+
if not relations:
|
|
176
|
+
raise InvalidOAuthRelationDataError("No OAuth provider is related.")
|
|
177
|
+
return relations[0].app.name
|