paas-charm 2.0.dev1__tar.gz → 2.0.dev4__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.dev4}/PKG-INFO +4 -1
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/pyproject.toml +5 -2
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/_gunicorn/webserver.py +2 -26
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/_gunicorn/workload_config.py +0 -6
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/app.py +193 -23
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/charm.py +162 -69
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/charm_state.py +81 -10
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/exceptions.py +9 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/expressjs/charm.py +0 -1
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/fastapi/app.py +4 -4
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/fastapi/charm.py +0 -1
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/go/charm.py +0 -1
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/oauth.py +15 -4
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/observability.py +7 -42
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/paas_config.py +107 -1
- paas_charm-2.0.dev4/src/paas_charm/relations.py +213 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/springboot/charm.py +10 -8
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/tracing.py +4 -2
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/utils.py +0 -19
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm.egg-info/PKG-INFO +4 -1
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm.egg-info/SOURCES.txt +1 -9
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm.egg-info/requires.txt +3 -0
- paas_charm-2.0.dev4/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.dev4}/README.md +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/setup.cfg +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/__init__.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/_gunicorn/__init__.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/_gunicorn/charm.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/_gunicorn/wsgi_app.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/charm_utils.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/database_migration.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/databases.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/django/__init__.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/django/charm.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/django/cos/grafana_dashboards/django.json +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/django/cos/loki_alert_rules/.gitkeep +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/django/cos/prometheus_alert_rules/django.rule +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/expressjs/__init__.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/expressjs/cos/grafana_dashboards/expressjs.json +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/expressjs/cos/loki_alert_rules/.gitkeep +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/expressjs/cos/prometheus_alert_rules/.gitkeep +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/fastapi/__init__.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/fastapi/cos/grafana_dashboards/fastapi.json +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/fastapi/cos/loki_alert_rules/.gitkeep +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/fastapi/cos/prometheus_alert_rules/.gitkeep +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/flask/__init__.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/flask/charm.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/flask/cos/grafana_dashboards/flask.json +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/flask/cos/loki_alert_rules/.gitkeep +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/flask/cos/prometheus_alert_rules/flask.rule +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/framework.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/go/__init__.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/go/cos/grafana_dashboards/go.json +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/go/cos/loki_alert_rules/.gitkeep +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/go/cos/prometheus_alert_rules/.gitkeep +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/http_proxy.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/peers.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/rabbitmq.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/s3.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/saml.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/secret_key.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/springboot/__init__.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/springboot/cos/grafana_dashboards/spring_boot.json +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/springboot/cos/loki_alert_rules/.gitkeep +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/springboot/cos/prometheus_alert_rules/.gitkeep +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/templates/_gunicorn_json_logging.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/templates/fastapi/uvicorn-log-config.json +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/templates/fastapi/uvicorn_log_handler.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/templates/gunicorn.conf.py.j2 +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/src/paas_charm/valkey.py +0 -0
- {paas_charm-2.0.dev1 → paas_charm-2.0.dev4}/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.dev4
|
|
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.dev4"
|
|
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",
|
|
@@ -37,7 +40,7 @@ where = ["src"]
|
|
|
37
40
|
|
|
38
41
|
[tool.uv.sources]
|
|
39
42
|
# opcli is published only from the charm-ci git repository, not PyPI.
|
|
40
|
-
opcli = { git = "https://github.com/canonical/charm-ci.git", tag = "v1.0.
|
|
43
|
+
opcli = { git = "https://github.com/canonical/charm-ci.git", tag = "v1.0.2" }
|
|
41
44
|
|
|
42
45
|
[tool.setuptools.package-data]
|
|
43
46
|
paas_charm = ["**/cos/**", "**/cos/**/.**", "**/*.j2", "**/templates/**/*.py", "**/templates/**/*.json"]
|
|
@@ -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,19 +9,20 @@ 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
|
|
16
16
|
|
|
17
17
|
from paas_charm.charm_state import CharmState
|
|
18
18
|
from paas_charm.database_migration import DatabaseMigration
|
|
19
|
-
from paas_charm.
|
|
19
|
+
from paas_charm.exceptions import PaasConfigError
|
|
20
|
+
from paas_charm.paas_config import EnvVarConfig, LoggingFormat, SecretEnvVarsConfig
|
|
20
21
|
|
|
21
22
|
logger = logging.getLogger(__name__)
|
|
22
23
|
|
|
23
24
|
if TYPE_CHECKING:
|
|
24
|
-
from
|
|
25
|
+
from charmlibs.interfaces.openfga import OpenfgaProviderAppData
|
|
25
26
|
from charms.smtp_integrator.v0.smtp import SmtpRelationData
|
|
26
27
|
|
|
27
28
|
from paas_charm.databases import PaaSDatabaseRelationData
|
|
@@ -52,7 +53,6 @@ class WorkloadConfig: # pylint: disable=too-many-instance-attributes
|
|
|
52
53
|
app_dir: the application directory in the application container.
|
|
53
54
|
state_dir: the directory in the application container to store states information.
|
|
54
55
|
service_name: the WSGI application pebble service name.
|
|
55
|
-
log_files: list of files to monitor.
|
|
56
56
|
metrics_path: path to scrape for metrics.
|
|
57
57
|
metrics_port: port on which the application exposes metrics.
|
|
58
58
|
unit_name: Name of the unit. Needed to know if schedulers should run here.
|
|
@@ -69,7 +69,6 @@ class WorkloadConfig: # pylint: disable=too-many-instance-attributes
|
|
|
69
69
|
app_dir: pathlib.Path
|
|
70
70
|
state_dir: pathlib.Path
|
|
71
71
|
service_name: str
|
|
72
|
-
log_files: List[pathlib.Path]
|
|
73
72
|
metrics_path: str | None = "/metrics"
|
|
74
73
|
metrics_port: int = 8080
|
|
75
74
|
unit_name: str
|
|
@@ -444,37 +443,198 @@ class App: # pylint: disable=too-many-instance-attributes
|
|
|
444
443
|
self._run_migrations()
|
|
445
444
|
self._container.replan()
|
|
446
445
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
446
|
+
def gen_environment(self) -> dict[str, str]:
|
|
447
|
+
"""Generate the final workload environment in precedence order.
|
|
448
|
+
|
|
449
|
+
Explicit config mappings override integrations, which override library-generated
|
|
450
|
+
variables. The config/framework layer comes from ``_framework_environment``.
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
The final environment with string-encoded values.
|
|
454
|
+
"""
|
|
455
|
+
framework_sources = self._framework_config_mapping_sources()
|
|
456
|
+
env = self._framework_environment()
|
|
457
|
+
for name, value in self._generate_integration_environments(
|
|
458
|
+
prefix=self.integrations_prefix
|
|
459
|
+
).items():
|
|
460
|
+
if name in env:
|
|
461
|
+
logger.warning(
|
|
462
|
+
"Environment variable %r for a relation or Prometheus metrics overwrites an "
|
|
463
|
+
"environment variable from charm configuration or framework settings",
|
|
464
|
+
name,
|
|
465
|
+
)
|
|
466
|
+
env[name] = value
|
|
467
|
+
for name, (source, value) in self._mapped_config_environment(framework_sources).items():
|
|
468
|
+
if name in env:
|
|
469
|
+
logger.warning(
|
|
470
|
+
"Explicit config.options mapping from %r overwrites environment variable %r",
|
|
471
|
+
source,
|
|
472
|
+
name,
|
|
473
|
+
)
|
|
474
|
+
env[name] = value
|
|
475
|
+
return env
|
|
476
|
+
|
|
477
|
+
def _mapped_framework_config_fields(self) -> set[str]:
|
|
478
|
+
"""Get framework fields whose default outputs are explicitly renamed.
|
|
479
|
+
|
|
480
|
+
Returns:
|
|
481
|
+
Framework field names, excluding unmapped secret-key entries.
|
|
482
|
+
"""
|
|
483
|
+
mappings = self._charm_state.config_options.options
|
|
484
|
+
return {
|
|
485
|
+
field
|
|
486
|
+
for option, field in self._charm_state.framework_config_fields.items()
|
|
487
|
+
if (mapping := mappings.get(option)) is not None
|
|
488
|
+
and (
|
|
489
|
+
isinstance(mapping, EnvVarConfig)
|
|
490
|
+
or (
|
|
491
|
+
field in ("secret_key", "app_secret_key")
|
|
492
|
+
and "value" in mapping.secret_env_vars
|
|
493
|
+
and not isinstance(self._charm_state.framework_config.get(field), dict)
|
|
494
|
+
)
|
|
495
|
+
)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
def _framework_config_mapping_sources(self) -> dict[str, str | dict[str, str]]:
|
|
499
|
+
"""Read resolved framework values without borrowing unrelated generated outputs.
|
|
500
|
+
|
|
501
|
+
Returns:
|
|
502
|
+
Values keyed by exact option names, using the existing encoding and key fallback.
|
|
503
|
+
"""
|
|
504
|
+
sources: dict[str, str | dict[str, str]] = {}
|
|
505
|
+
for option, mapping in self._charm_state.config_options.options.items():
|
|
506
|
+
field = self._charm_state.framework_config_fields.get(option)
|
|
507
|
+
if field is None:
|
|
508
|
+
continue
|
|
509
|
+
value = self._charm_state.framework_config.get(field)
|
|
510
|
+
if value is None and field in ("secret_key", "app_secret_key"):
|
|
511
|
+
value = self._charm_state.secret_key
|
|
512
|
+
if value is None:
|
|
513
|
+
continue
|
|
514
|
+
if isinstance(mapping, SecretEnvVarsConfig):
|
|
515
|
+
if isinstance(value, dict):
|
|
516
|
+
sources[option] = value
|
|
517
|
+
elif field in ("secret_key", "app_secret_key"):
|
|
518
|
+
sources[option] = {"value": encode_env(value)}
|
|
519
|
+
else:
|
|
520
|
+
sources[option] = encode_env(value)
|
|
521
|
+
else:
|
|
522
|
+
sources[option] = encode_env(value)
|
|
523
|
+
return sources
|
|
524
|
+
|
|
525
|
+
def _mapped_config_environment(
|
|
526
|
+
self, framework_sources: dict[str, str | dict[str, str]]
|
|
527
|
+
) -> dict[str, tuple[str, str]]:
|
|
528
|
+
"""Generate explicit config destinations from their original source values.
|
|
529
|
+
|
|
530
|
+
Args:
|
|
531
|
+
framework_sources: Resolved framework outputs keyed by exact option names,
|
|
532
|
+
including secret-key fallbacks.
|
|
461
533
|
|
|
462
534
|
Returns:
|
|
463
|
-
|
|
535
|
+
Destinations mapped to source names and encoded values, omitting absent values.
|
|
536
|
+
|
|
537
|
+
Raises:
|
|
538
|
+
PaasConfigError: If a secret mapping receives non-secret content.
|
|
539
|
+
"""
|
|
540
|
+
env: dict[str, tuple[str, str]] = {}
|
|
541
|
+
for option, mapping in self._charm_state.config_options.options.items():
|
|
542
|
+
value = (
|
|
543
|
+
framework_sources.get(option)
|
|
544
|
+
if option in self._charm_state.framework_config_fields
|
|
545
|
+
else self._charm_state.user_defined_config.get(option.replace("-", "_"))
|
|
546
|
+
)
|
|
547
|
+
if value is None:
|
|
548
|
+
continue
|
|
549
|
+
if isinstance(mapping, SecretEnvVarsConfig):
|
|
550
|
+
if not isinstance(value, collections.abc.Mapping):
|
|
551
|
+
raise PaasConfigError(
|
|
552
|
+
f"config.options.{option}.secret-env-vars requires secret content"
|
|
553
|
+
)
|
|
554
|
+
for key, destination in mapping.secret_env_vars.items():
|
|
555
|
+
if key not in value:
|
|
556
|
+
logger.warning(
|
|
557
|
+
"Charm configuration option %r has no secret content key %r; "
|
|
558
|
+
"skipping mapped environment variable %r",
|
|
559
|
+
option,
|
|
560
|
+
key,
|
|
561
|
+
destination,
|
|
562
|
+
)
|
|
563
|
+
continue
|
|
564
|
+
env[destination] = (f"{option}.{key}", encode_env(value[key]))
|
|
565
|
+
else:
|
|
566
|
+
env[mapping.env_var] = (option, encode_env(value))
|
|
567
|
+
return env
|
|
568
|
+
|
|
569
|
+
def _unmapped_framework_config(self) -> dict[str, str | int | bool | dict[str, str]]:
|
|
570
|
+
"""Keep framework defaults and secret entries that have not been renamed.
|
|
571
|
+
|
|
572
|
+
Returns:
|
|
573
|
+
Resolved framework configuration with mapped fields or secret entries omitted.
|
|
574
|
+
"""
|
|
575
|
+
mapped_fields = self._mapped_framework_config_fields()
|
|
576
|
+
config = {
|
|
577
|
+
field: value
|
|
578
|
+
for field, value in self._charm_state.framework_config.items()
|
|
579
|
+
if field not in mapped_fields
|
|
580
|
+
}
|
|
581
|
+
for option, field in self._charm_state.framework_config_fields.items():
|
|
582
|
+
mapping = self._charm_state.config_options.options.get(option)
|
|
583
|
+
value = config.get(field)
|
|
584
|
+
if (
|
|
585
|
+
isinstance(mapping, SecretEnvVarsConfig)
|
|
586
|
+
and isinstance(value, dict)
|
|
587
|
+
and value.keys() & mapping.secret_env_vars.keys()
|
|
588
|
+
):
|
|
589
|
+
remaining = {
|
|
590
|
+
key: val for key, val in value.items() if key not in mapping.secret_env_vars
|
|
591
|
+
}
|
|
592
|
+
if remaining:
|
|
593
|
+
config[field] = remaining
|
|
594
|
+
else:
|
|
595
|
+
config.pop(field)
|
|
596
|
+
return config
|
|
597
|
+
|
|
598
|
+
def _framework_environment(self) -> dict[str, str]: # noqa: too-complex
|
|
599
|
+
"""Build config and library-owned variables before higher-priority overrides.
|
|
600
|
+
|
|
601
|
+
Explicitly mapped sources are omitted from their default names. Unmapped config
|
|
602
|
+
retains its existing prefixes and value encoding. Framework subclasses extend
|
|
603
|
+
this layer; ``gen_environment`` then applies relations and explicit mappings.
|
|
604
|
+
|
|
605
|
+
Returns:
|
|
606
|
+
The config/framework environment with string-encoded values.
|
|
464
607
|
"""
|
|
465
608
|
prefix = self.configuration_prefix
|
|
466
609
|
env = {}
|
|
610
|
+
mappings = {
|
|
611
|
+
option.replace("-", "_"): mapping
|
|
612
|
+
for option, mapping in self._charm_state.config_options.options.items()
|
|
613
|
+
}
|
|
467
614
|
for app_config_key, app_config_value in self._charm_state.user_defined_config.items():
|
|
615
|
+
mapping = mappings.get(app_config_key)
|
|
616
|
+
if app_config_key in self._charm_state.framework_config_field_names or isinstance(
|
|
617
|
+
mapping, EnvVarConfig
|
|
618
|
+
):
|
|
619
|
+
continue
|
|
620
|
+
if isinstance(mapping, SecretEnvVarsConfig) and app_config_value is None:
|
|
621
|
+
continue
|
|
622
|
+
mapped_keys = (
|
|
623
|
+
mapping.secret_env_vars if isinstance(mapping, SecretEnvVarsConfig) else {}
|
|
624
|
+
)
|
|
468
625
|
if isinstance(app_config_value, collections.abc.Mapping):
|
|
469
626
|
for k, v in app_config_value.items():
|
|
627
|
+
if k in mapped_keys:
|
|
628
|
+
continue
|
|
470
629
|
env[f"{prefix}{app_config_key.upper()}_{k.replace('-', '_').upper()}"] = (
|
|
471
630
|
encode_env(v)
|
|
472
631
|
)
|
|
473
632
|
else:
|
|
474
633
|
env[f"{prefix}{app_config_key.upper()}"] = encode_env(app_config_value)
|
|
475
634
|
|
|
476
|
-
framework_config = self.
|
|
635
|
+
framework_config = self._unmapped_framework_config()
|
|
477
636
|
framework_config_prefix = self.framework_config_prefix
|
|
637
|
+
mapped_fields = self._mapped_framework_config_fields()
|
|
478
638
|
env.update(
|
|
479
639
|
{
|
|
480
640
|
f"{framework_config_prefix}{k.upper()}": encode_env(v)
|
|
@@ -489,7 +649,7 @@ class App: # pylint: disable=too-many-instance-attributes
|
|
|
489
649
|
if self._charm_state.base_url:
|
|
490
650
|
env[f"{prefix}BASE_URL"] = self._charm_state.base_url
|
|
491
651
|
secret_key_env = f"{prefix}SECRET_KEY"
|
|
492
|
-
if secret_key_env not in env:
|
|
652
|
+
if secret_key_env not in env and not {"secret_key", "app_secret_key"} & mapped_fields:
|
|
493
653
|
env[secret_key_env] = self._charm_state.secret_key
|
|
494
654
|
for proxy_variable in ("http_proxy", "https_proxy", "no_proxy"):
|
|
495
655
|
proxy_value = getattr(self._charm_state.proxy, proxy_variable)
|
|
@@ -500,7 +660,6 @@ class App: # pylint: disable=too-many-instance-attributes
|
|
|
500
660
|
if self._charm_state.peer_fqdns is not None:
|
|
501
661
|
env[f"{prefix}PEER_FQDNS"] = self._charm_state.peer_fqdns
|
|
502
662
|
|
|
503
|
-
env.update(self._generate_integration_environments(prefix=self.integrations_prefix))
|
|
504
663
|
return env
|
|
505
664
|
|
|
506
665
|
def _generate_integration_environments(self, prefix: str = "") -> dict[str, str]:
|
|
@@ -531,6 +690,17 @@ class App: # pylint: disable=too-many-instance-attributes
|
|
|
531
690
|
relation_data=self._charm_state.integrations.oauth,
|
|
532
691
|
)
|
|
533
692
|
)
|
|
693
|
+
|
|
694
|
+
for relation in self._charm_state.custom_relations:
|
|
695
|
+
for key, value in (relation.gen_environment() or {}).items():
|
|
696
|
+
if key in env:
|
|
697
|
+
logger.warning(
|
|
698
|
+
"Custom relation %s overwrites environment variable %s",
|
|
699
|
+
relation.relation_name,
|
|
700
|
+
key,
|
|
701
|
+
)
|
|
702
|
+
env[key] = value
|
|
703
|
+
|
|
534
704
|
return {prefix + k: v for (k, v) in env.items()}
|
|
535
705
|
|
|
536
706
|
@property
|