Skip to content
Draft
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
16 changes: 16 additions & 0 deletions content-cache-backends-config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,19 @@ options:
(https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_cache_valid).
type: string
default: '[]'
cache-inactive:
description: |
Time after which a cached item is evicted from disk if not accessed.
Uses nginx time format: positive integer followed by s (seconds),
m (minutes), h (hours), or d (days). For example: 10m, 1h, 7d.
Increase for files that are accessed on long periodic cycles.
type: string
default: "10m"
cache-max-size:
description: |
Maximum total disk space used by the cache. Uses nginx size format:
positive integer followed by k, m, g, or t (case-insensitive).
For example: 512m, 2g. When set, nginx evicts least-recently-used
entries when the limit is reached. Empty string means no limit.
type: string
default: ""
85 changes: 83 additions & 2 deletions content-cache-backends-config/src/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
HEALTHCHECK_SSL_VERIFY_CONFIG_NAME = "healthcheck-ssl-verify"
HEALTHCHECK_VALID_STATUS_CONFIG_NAME = "healthcheck-valid-status"
PROXY_CACHE_VALID_CONFIG_NAME = "proxy-cache-valid"
CACHE_INACTIVE_CONFIG_NAME = "cache-inactive"
CACHE_MAX_SIZE_CONFIG_NAME = "cache-max-size"
CACHE_INACTIVE_FIELD_NAME = "cache_inactive"
CACHE_MAX_SIZE_FIELD_NAME = "cache_max_size"


def _validate_path_value(value: str) -> str:
Expand Down Expand Up @@ -116,12 +120,16 @@ class Configuration(pydantic.BaseModel):
fail_timeout: The time to wait before using a backend after failure.
proxy_cache_valid: The cache valid duration.
healthcheck: The healthcheck configuration.
cache_inactive: Time after which an unaccessed item is evicted from the disk cache.
cache_max_size: Maximum total disk size for the cache; empty string means no limit.
"""

backends: tuple[pydantic.AnyHttpUrl, ...]
fail_timeout: typing.Annotated[str, pydantic.StringConstraints(min_length=1)]
proxy_cache_valid: tuple[str, ...]
healthcheck: HealthcheckConfig
cache_inactive: str
cache_max_size: str

@pydantic.field_validator("backends")
@classmethod
Expand Down Expand Up @@ -171,6 +179,48 @@ def validate_proxy_cache_valid(cls, value: tuple[str, ...]) -> tuple[str, ...]:
_check_nginx_time_str(time_str)
return value

@pydantic.field_validator("cache_inactive")
@classmethod
def validate_cache_inactive(cls, value: str) -> str:
"""Validate the cache_inactive time string.

Args:
value: The nginx time string to validate.

Raises:
ValueError: The value is not a valid nginx time string.

Returns:
The validated value.
"""
try:
_check_nginx_time_str(value)
except ValueError as exc:
raise ValueError(str(exc)) from exc
return value

@pydantic.field_validator("cache_max_size")
@classmethod
def validate_cache_max_size(cls, value: str) -> str:
"""Validate the cache_max_size size string.

Args:
value: The nginx size string to validate (may be empty to mean no limit).

Raises:
ValueError: The value is not empty and not a valid nginx size string.

Returns:
The validated value, lowercased.
"""
if not value:
return value
try:
_check_nginx_size_str(value)
except ValueError as exc:
raise ValueError(str(exc)) from exc
return value.lower()

@classmethod
def from_charm(cls, charm: ops.CharmBase) -> "Configuration":
"""Initialize object from the charm.
Expand Down Expand Up @@ -206,13 +256,20 @@ def from_charm(cls, charm: ops.CharmBase) -> "Configuration":

healthcheck_config = HealthcheckConfig.from_charm(charm)

cache_inactive = typing.cast(
str, charm.config.get(CACHE_INACTIVE_CONFIG_NAME, "10m")
).strip()
cache_max_size = typing.cast(str, charm.config.get(CACHE_MAX_SIZE_CONFIG_NAME, "")).strip()

try:
# Ignore type check and let pydantic handle the type with validation errors.
return cls(
backends=backends, # type: ignore
fail_timeout=fail_timeout,
proxy_cache_valid=proxy_cache_valid, # type: ignore
healthcheck=healthcheck_config,
cache_inactive=cache_inactive,
cache_max_size=cache_max_size,
)
except pydantic.ValidationError as err:
err_msg = [
Expand Down Expand Up @@ -272,8 +329,8 @@ def _check_nginx_time_str(time_str: str) -> None:
Raises:
ValueError: The input is not valid time str for nginx.
"""
time_char = {"h", "m", "s"}
if time_str[-1] not in time_char:
time_char = {"h", "m", "s", "d"}
if not time_str or time_str[-1] not in time_char:
raise ValueError(f"Invalid time for proxy_cache_valid: {time_str}")
try:
time = int(time_str[:-1])
Expand All @@ -284,6 +341,30 @@ def _check_nginx_time_str(time_str: str) -> None:
raise ValueError(f"Time must be positive int for proxy_cache_valid: {time_str}")


def _check_nginx_size_str(size_str: str) -> None:
"""Check if nginx size string is valid.

Valid format: positive integer followed by k, m, g, or t (case-insensitive).

Args:
size_str: The size string to validate.

Raises:
ValueError: The input is not a valid nginx size string.
"""
if not size_str:
raise ValueError("Size string must not be empty")
unit = size_str[-1].lower()
if unit not in {"k", "m", "g", "t"}:
raise ValueError(f"Invalid size unit in {size_str!r}: must be k, m, g, or t")
try:
value = int(size_str[:-1])
except ValueError as err:
raise ValueError(f"Non-integer size value in {size_str!r}") from err
if value < 1:
raise ValueError(f"Size must be a positive integer in {size_str!r}")


def _check_status_code(code_str: str) -> None:
"""Check if status code is valid.

Expand Down
22 changes: 9 additions & 13 deletions content-cache-backends-config/tests/unit/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,18 @@
"""Module for defining unit test fixtures."""

import pytest
from ops.testing import Harness
import scenario

from charm import ContentCacheBackendsConfigCharm


@pytest.fixture(name="harness", scope="function")
def harness_fixture():
"""The ops testing harness fixture."""
harness = Harness(ContentCacheBackendsConfigCharm)
harness.set_leader(True)
harness.begin_with_initial_hooks()
yield harness
harness.cleanup()
@pytest.fixture(name="ctx")
def context_fixture() -> scenario.Context:
"""A scenario Context for ContentCacheBackendsConfigCharm."""
return scenario.Context(ContentCacheBackendsConfigCharm)


@pytest.fixture(name="charm", scope="function")
def charm_fixture(harness: Harness):
"""The charm fixture"""
return harness.charm
@pytest.fixture(name="ctx_leader")
def context_leader_fixture() -> scenario.Context:
"""A scenario Context for ContentCacheBackendsConfigCharm (leader)."""
return scenario.Context(ContentCacheBackendsConfigCharm)
4 changes: 4 additions & 0 deletions content-cache-backends-config/tests/unit/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

from src.state import (
BACKENDS_CONFIG_NAME,
CACHE_INACTIVE_CONFIG_NAME,
CACHE_MAX_SIZE_CONFIG_NAME,
FAIL_TIMEOUT_CONFIG_NAME,
HEALTHCHECK_INTERVAL_CONFIG_NAME,
HEALTHCHECK_PATH_CONFIG_NAME,
Expand Down Expand Up @@ -56,5 +58,7 @@ class Meta:
HEALTHCHECK_SSL_VERIFY_CONFIG_NAME: False,
HEALTHCHECK_VALID_STATUS_CONFIG_NAME: "200",
PROXY_CACHE_VALID_CONFIG_NAME: "[]",
CACHE_INACTIVE_CONFIG_NAME: "10m",
CACHE_MAX_SIZE_CONFIG_NAME: "",
}
)
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
factory-boy>=3,<4
ops-scenario>=7.0.0,<9.0.0
Loading
Loading