Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add secret configurations #46

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions examples/django/charm/charmcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ config:
for any other security related needs by your Django application. This configuration
will set the DJANGO_SECRET_KEY environment variable.
type: string
django-secret-key-id:
description: >-
This configuration is similar to `django-secret-key`, but instead accepts a Juju user secret ID.
The secret should contain a single key, "value", which maps to the actual Django secret key.
To create the secret, run the following command:
`juju add-secret my-django-secret-key value=<secret-string> && juju grant-secret my-django-secret-key django-k8s`,
and use the outputted secret ID to configure this option.
type: secret
webserver-keepalive:
description: Time in seconds for webserver to wait for requests on a Keep-Alive
connection.
Expand Down
8 changes: 8 additions & 0 deletions examples/fastapi/charm/charmcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ config:
type: string
description: Long secret you can use for sessions, csrf or any other thing where
you need a random secret shared by all units
app-secret-key-id:
type: secret
description: >-
This configuration is similar to `app-secret-key`, but instead accepts a Juju user secret ID.
The secret should contain a single key, "value", which maps to the actual secret key.
To create the secret, run the following command:
`juju add-secret my-secret-key value=<secret-string> && juju grant-secret my-secret-key fastapi-k8s`,
and use the outputted secret ID to configure this option.
user-defined-config:
type: string
description: Example of a user defined configuration.
Expand Down
11 changes: 11 additions & 0 deletions examples/flask/charmcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ config:
will set the FLASK_SECRET_KEY environment variable. Run `app.config.from_prefixed_env()`
in your Flask application in order to receive this configuration.
type: string
flask-secret-key-id:
description: >-
This configuration is similar to `flask-secret-key`, but instead accepts a Juju user secret ID.
The secret should contain a single key, "value", which maps to the actual Flask secret key.
To create the secret, run the following command:
`juju add-secret my-flask-secret-key value=<secret-string> && juju grant-secret my-flask-secret-key flask-k8s`,
and use the outputted secret ID to configure this option.
type: secret
flask-session-cookie-secure:
description: Set the secure attribute in the Flask application cookies. This
configuration will set the FLASK_SESSION_COOKIE_SECURE environment variable.
Expand All @@ -74,6 +82,9 @@ config:
webserver-workers:
description: The number of webserver worker processes for handling requests.
type: int
secret-test:
description: A test configuration option for testing user provided Juju secrets.
type: secret
containers:
flask-app:
resource: flask-app-image
Expand Down
8 changes: 8 additions & 0 deletions examples/go/charm/charmcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ config:
type: string
description: Long secret you can use for sessions, csrf or any other thing where
you need a random secret shared by all units
app-secret-key-id:
type: secret
description: >-
This configuration is similar to `app-secret-key`, but instead accepts a Juju user secret ID.
The secret should contain a single key, "value", which maps to the actual secret key.
To create the secret, run the following command:
`juju add-secret my-secret-key value=<secret-string> && juju grant-secret my-secret-key go-k8s`,
and use the outputted secret ID to configure this option.
user-defined-config:
type: string
description: Example of a user defined configuration.
Expand Down
13 changes: 10 additions & 3 deletions paas_app_charmer/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# See LICENSE file for licensing details.

"""Provide the base generic class to represent the application."""

import collections
import json
import logging
import pathlib
Expand Down Expand Up @@ -118,9 +118,16 @@ def gen_environment(self) -> dict[str, str]:
Returns:
A dictionary representing the application environment variables.
"""
config = self._charm_state.app_config
prefix = self.configuration_prefix
env = {f"{prefix}{k.upper()}": encode_env(v) for k, v in config.items()}
env = {}
for app_config_key, app_config_value in self._charm_state.app_config.items():
if isinstance(app_config_value, collections.abc.Mapping):
for k, v in app_config_value.items():
env[f"{prefix}{app_config_key.upper()}_{k.replace('-', '_').upper()}"] = (
encode_env(v)
)
else:
env[f"{prefix}{app_config_key.upper()}"] = encode_env(app_config_value)

framework_config = self._charm_state.framework_config
framework_config_prefix = self.framework_config_prefix
Expand Down
56 changes: 27 additions & 29 deletions paas_app_charmer/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from paas_app_charmer.observability import Observability
from paas_app_charmer.rabbitmq import RabbitMQRequires
from paas_app_charmer.secret_storage import KeySecretStorage
from paas_app_charmer.utils import build_validation_error_message
from paas_app_charmer.utils import build_validation_error_message, config_get_3

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -143,6 +143,7 @@ def __init__(self, framework: ops.Framework, framework_name: str) -> None:
self._on_secret_storage_relation_changed,
)
self.framework.observe(self.on.update_status, self._on_update_status)
self.framework.observe(self.on.secret_changed, self._on_secret_changed)
for database, database_requirer in self._database_requirers.items():
self.framework.observe(
database_requirer.on.database_created,
Expand Down Expand Up @@ -173,7 +174,7 @@ def get_framework_config(self) -> BaseModel:
"""
# Will raise an AttributeError if it the attribute framework_config_class does not exist.
framework_config_class = self.framework_config_class
config = dict(self.config.items())
config = {k: config_get_3(self, k) for k in self.config.keys()}
try:
return framework_config_class.model_validate(config)
except ValidationError as exc:
Expand All @@ -186,12 +187,13 @@ def _container(self) -> Container:
return self.unit.get_container(self._workload_config.container_name)

@block_if_invalid_config
def _on_config_changed(self, _event: ops.EventBase) -> None:
"""Configure the application pebble service layer.
def _on_config_changed(self, _: ops.EventBase) -> None:
"""Configure the application pebble service layer."""
self.restart()

Args:
_event: the config-changed event that triggers this callback function.
"""
@block_if_invalid_config
def _on_secret_changed(self, _: ops.EventBase) -> None:
"""Configure the application Pebble service layer."""
self.restart()

@block_if_invalid_config
Expand All @@ -212,12 +214,8 @@ def _on_rotate_secret_key_action(self, event: ops.ActionEvent) -> None:
self.restart()

@block_if_invalid_config
def _on_secret_storage_relation_changed(self, _event: ops.RelationEvent) -> None:
"""Handle the secret-storage-relation-changed event.

Args:
_event: the action event that triggers this callback.
"""
def _on_secret_storage_relation_changed(self, _: ops.RelationEvent) -> None:
"""Handle the secret-storage-relation-changed event."""
self.restart()

def update_app_and_unit_status(self, status: ops.StatusBase) -> None:
Expand Down Expand Up @@ -360,67 +358,67 @@ def _on_update_status(self, _: ops.HookEvent) -> None:
self.restart()

@block_if_invalid_config
def _on_mysql_database_database_created(self, _event: DatabaseRequiresEvent) -> None:
def _on_mysql_database_database_created(self, _: DatabaseRequiresEvent) -> None:
"""Handle mysql's database-created event."""
self.restart()

@block_if_invalid_config
def _on_mysql_database_endpoints_changed(self, _event: DatabaseRequiresEvent) -> None:
def _on_mysql_database_endpoints_changed(self, _: DatabaseRequiresEvent) -> None:
"""Handle mysql's endpoints-changed event."""
self.restart()

@block_if_invalid_config
def _on_mysql_database_relation_broken(self, _event: ops.RelationBrokenEvent) -> None:
def _on_mysql_database_relation_broken(self, _: ops.RelationBrokenEvent) -> None:
"""Handle mysql's relation-broken event."""
self.restart()

@block_if_invalid_config
def _on_postgresql_database_database_created(self, _event: DatabaseRequiresEvent) -> None:
def _on_postgresql_database_database_created(self, _: DatabaseRequiresEvent) -> None:
"""Handle postgresql's database-created event."""
self.restart()

@block_if_invalid_config
def _on_postgresql_database_endpoints_changed(self, _event: DatabaseRequiresEvent) -> None:
def _on_postgresql_database_endpoints_changed(self, _: DatabaseRequiresEvent) -> None:
"""Handle mysql's endpoints-changed event."""
self.restart()

@block_if_invalid_config
def _on_postgresql_database_relation_broken(self, _event: ops.RelationBrokenEvent) -> None:
def _on_postgresql_database_relation_broken(self, _: ops.RelationBrokenEvent) -> None:
"""Handle postgresql's relation-broken event."""
self.restart()

@block_if_invalid_config
def _on_mongodb_database_database_created(self, _event: DatabaseRequiresEvent) -> None:
def _on_mongodb_database_database_created(self, _: DatabaseRequiresEvent) -> None:
"""Handle mongodb's database-created event."""
self.restart()

@block_if_invalid_config
def _on_mongodb_database_endpoints_changed(self, _event: DatabaseRequiresEvent) -> None:
def _on_mongodb_database_endpoints_changed(self, _: DatabaseRequiresEvent) -> None:
"""Handle mysql's endpoints-changed event."""
self.restart()

@block_if_invalid_config
def _on_mongodb_database_relation_broken(self, _event: ops.RelationBrokenEvent) -> None:
def _on_mongodb_database_relation_broken(self, _: ops.RelationBrokenEvent) -> None:
"""Handle postgresql's relation-broken event."""
self.restart()

@block_if_invalid_config
def _on_redis_relation_updated(self, _event: DatabaseRequiresEvent) -> None:
def _on_redis_relation_updated(self, _: DatabaseRequiresEvent) -> None:
"""Handle redis's database-created event."""
self.restart()

@block_if_invalid_config
def _on_s3_credential_changed(self, _event: ops.HookEvent) -> None:
def _on_s3_credential_changed(self, _: ops.HookEvent) -> None:
"""Handle s3 credentials-changed event."""
self.restart()

@block_if_invalid_config
def _on_s3_credential_gone(self, _event: ops.HookEvent) -> None:
def _on_s3_credential_gone(self, _: ops.HookEvent) -> None:
"""Handle s3 credentials-gone event."""
self.restart()

@block_if_invalid_config
def _on_saml_data_available(self, _event: ops.HookEvent) -> None:
def _on_saml_data_available(self, _: ops.HookEvent) -> None:
"""Handle saml data available event."""
self.restart()

Expand All @@ -440,16 +438,16 @@ def _on_pebble_ready(self, _: ops.PebbleReadyEvent) -> None:
self.restart()

@block_if_invalid_config
def _on_rabbitmq_connected(self, _event: ops.HookEvent) -> None:
def _on_rabbitmq_connected(self, _: ops.HookEvent) -> None:
"""Handle rabbitmq connected event."""
self.restart()

@block_if_invalid_config
def _on_rabbitmq_ready(self, _event: ops.HookEvent) -> None:
def _on_rabbitmq_ready(self, _: ops.HookEvent) -> None:
"""Handle rabbitmq ready event."""
self.restart()

@block_if_invalid_config
def _on_rabbitmq_departed(self, _event: ops.HookEvent) -> None:
def _on_rabbitmq_departed(self, _: ops.HookEvent) -> None:
"""Handle rabbitmq departed event."""
self.restart()
12 changes: 6 additions & 6 deletions paas_app_charmer/charm_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from paas_app_charmer.databases import get_uri
from paas_app_charmer.exceptions import CharmConfigInvalidError
from paas_app_charmer.secret_storage import KeySecretStorage
from paas_app_charmer.utils import build_validation_error_message
from paas_app_charmer.utils import build_validation_error_message, config_get_3

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -52,7 +52,7 @@ def __init__( # pylint: disable=too-many-arguments
*,
framework: str,
is_secret_storage_ready: bool,
app_config: dict[str, int | str | bool] | None = None,
app_config: dict[str, int | str | bool | dict[str, str]] | None = None,
framework_config: dict[str, int | str] | None = None,
secret_key: str | None = None,
integrations: "IntegrationsState | None" = None,
Expand Down Expand Up @@ -109,8 +109,8 @@ def from_charm( # pylint: disable=too-many-arguments
The CharmState instance created by the provided charm.
"""
app_config = {
k.replace("-", "_"): v
for k, v in charm.config.items()
k.replace("-", "_"): config_get_3(charm, k)
for k in charm.config.keys()
if not any(k.startswith(prefix) for prefix in (f"{framework}-", "webserver-", "app-"))
}
app_config = {
Expand All @@ -127,7 +127,7 @@ def from_charm( # pylint: disable=too-many-arguments
return cls(
framework=framework,
framework_config=framework_config.dict(exclude_none=True),
app_config=typing.cast(dict[str, str | int | bool], app_config),
app_config=typing.cast(dict[str, str | int | bool | dict[str, str]], app_config),
secret_key=(
secret_storage.get_secret_key() if secret_storage.is_initialized else None
),
Expand Down Expand Up @@ -162,7 +162,7 @@ def framework_config(self) -> dict[str, str | int | bool]:
return self._framework_config

@property
def app_config(self) -> dict[str, str | int | bool]:
def app_config(self) -> dict[str, str | int | bool | dict[str, str]]:
"""Get the value of user-defined application configurations.

Returns:
Expand Down
8 changes: 6 additions & 2 deletions paas_app_charmer/django/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,31 @@
import typing

import ops
from pydantic import BaseModel, Extra, Field, validator
from pydantic import ConfigDict, Field, validator

from paas_app_charmer._gunicorn.charm import GunicornBase
from paas_app_charmer.framework import FrameworkConfig

logger = logging.getLogger(__name__)


class DjangoConfig(BaseModel, extra=Extra.ignore):
class DjangoConfig(FrameworkConfig):
"""Represent Django builtin configuration values.

Attrs:
debug: whether Django debug mode is enabled.
secret_key: a secret key that will be used for security related needs by your
Django application.
allowed_hosts: a list of host/domain names that this Django site can serve.
model_config: Pydantic model configuration.
"""

debug: bool | None = Field(alias="django-debug", default=None)
secret_key: str | None = Field(alias="django-secret-key", default=None, min_length=1)
allowed_hosts: str | None = Field(alias="django-allowed-hosts", default=[])

model_config = ConfigDict(extra="ignore")

@validator("allowed_hosts")
@classmethod
def allowed_hosts_to_list(cls, value: str | None) -> typing.List[str]:
Expand Down
8 changes: 6 additions & 2 deletions paas_app_charmer/fastapi/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@
import typing

import ops
from pydantic import BaseModel, Extra, Field
from pydantic import ConfigDict, Field

from paas_app_charmer.app import App, WorkloadConfig
from paas_app_charmer.charm import PaasCharm
from paas_app_charmer.framework import FrameworkConfig


class FastAPIConfig(BaseModel, extra=Extra.ignore):
class FastAPIConfig(FrameworkConfig):
"""Represent FastAPI builtin configuration values.

Attrs:
Expand All @@ -25,6 +26,7 @@ class FastAPIConfig(BaseModel, extra=Extra.ignore):
metrics_path: path where the metrics are collected
app_secret_key: a secret key that will be used for securely signing the session cookie
and can be used for any other security related needs by your Flask application.
model_config: Pydantic model configuration.
"""

uvicorn_port: int = Field(alias="webserver-port", default=8080, gt=0)
Expand All @@ -37,6 +39,8 @@ class FastAPIConfig(BaseModel, extra=Extra.ignore):
metrics_path: str | None = Field(alias="metrics-path", default=None, min_length=1)
app_secret_key: str | None = Field(alias="app-secret-key", default=None, min_length=1)

model_config = ConfigDict(extra="ignore")


class Charm(PaasCharm):
"""FastAPI Charm service.
Expand Down
Loading
Loading