Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b9c4a3b
fix: Add vault-k8s library
Gu1nness Mar 12, 2026
fa0fcee
feat(DPE-9339): initial version of the code
Gu1nness Mar 12, 2026
22fdfbe
chore: add images
Gu1nness Mar 12, 2026
7f4f88e
fix: remove invalid comment
Gu1nness Mar 13, 2026
0bbb59d
fix: config and relation for test charm
Gu1nness Mar 13, 2026
3ed1c62
fix: refresh versions
Gu1nness Mar 13, 2026
984922d
test: add unit tests (basic)
Gu1nness Mar 13, 2026
b018ab8
chore: final unit tests
Gu1nness Mar 17, 2026
086bb77
chore: more unit tests
Gu1nness Mar 17, 2026
6b40e72
feat: basic integration tests
Gu1nness Mar 18, 2026
f25bd77
fix: progress on tests
Gu1nness Mar 18, 2026
b0418cb
fix: changes raised by IT
Gu1nness Mar 18, 2026
63394ec
fix: bump snap revision
Gu1nness Mar 18, 2026
b722edb
fix: tests
Gu1nness Mar 19, 2026
2e88cb9
fix: tests
Gu1nness Mar 19, 2026
1e5e38e
fix: refine user usage
Gu1nness Mar 19, 2026
b0868e7
fix: final UX + tests
Gu1nness Mar 19, 2026
927fab7
fix: add doc
Gu1nness Mar 19, 2026
598f3cc
Merge branch '8/edge' into DPE-9339-mongo-db-encryption-at-rest-imple…
Gu1nness Mar 19, 2026
6b6d752
fix: add status
Gu1nness Mar 19, 2026
be5d280
fix: unit tests
Gu1nness Mar 19, 2026
99f74fa
fix: unit tests
Gu1nness Mar 19, 2026
abcf4d9
Merge branch '8/edge' into DPE-9339-mongo-db-encryption-at-rest-imple…
Gu1nness Apr 1, 2026
ed5c7b2
fix: post-review
Gu1nness Apr 1, 2026
76ade2c
fix: post-review
Gu1nness Apr 1, 2026
42ffda6
fix: bump mypy
Gu1nness Apr 1, 2026
0b366c6
fix: remove duplicated entry
Gu1nness Apr 1, 2026
19fd804
fix: typing
Gu1nness Apr 1, 2026
751c8e1
Update single_kernel_mongo/managers/config.py
Gu1nness Apr 2, 2026
c1f2758
Update single_kernel_mongo/state/vault_state.py
Gu1nness Apr 2, 2026
7ad315e
Update single_kernel_mongo/state/vault_state.py
Gu1nness Apr 2, 2026
e79859d
fix: post-review
Gu1nness Apr 2, 2026
5617989
fix: post-review
Gu1nness Apr 2, 2026
7fbd89d
Merge branch '8/edge' into DPE-9339-mongo-db-encryption-at-rest-imple…
Gu1nness Apr 2, 2026
b677c05
Merge remote-tracking branch 'origin/8/edge' into DPE-9339-mongo-db-e…
Gu1nness Apr 8, 2026
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
12 changes: 6 additions & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,24 +49,24 @@ repos:
- id: ruff-format

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.13.0
rev: v1.19.1
hooks:
- id: mypy
language: system
args: ["--config-file=pyproject.toml","."]
args: ["--config-file=pyproject.toml", "."]
pass_filenames: false

- repo: https://github.com/codespell-project/codespell
rev: v2.2.6
hooks:
- id: codespell
additional_dependencies: [tomli]
args: ["--write-changes"]
- id: codespell
additional_dependencies: [tomli]
args: ["--write-changes"]

- repo: https://github.com/rhysd/actionlint
rev: v1.7.7
hooks:
- id: actionlint
- id: actionlint

exclude: |
(?x)(
Expand Down
423 changes: 326 additions & 97 deletions poetry.lock

Large diffs are not rendered by default.

13 changes: 9 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

[project]
name = "mongo-charms-single-kernel"
dynamic = ["version"]
dynamic = []
description = "Shared and reusable code for Mongo-related charms"
readme = "README.md"
license = "Apache-2.0"
Expand Down Expand Up @@ -40,8 +40,12 @@ dependencies = [
"python-ldap",
"charm-refresh (>=3.1.0.2,<4.0.0.0)",
"google-cloud-storage (~=2.16.0)",
"google-api-core (~=2.17.0)"
"google-api-core (~=2.17.0)",
"pytest-interface-tester (>=3.4.1,<4.0.0)",
"hvac (>=2.4.0,<3.0.0)",
"python-hcl2"
]
version = "1.8.32.post7.dev0+3ed1c6295.dirty"

[project.urls]
homepage = "https://github.com/canonical/mongo-single-kernel-library"
Expand All @@ -58,14 +62,13 @@ packages = [
]
include = ["single_kernel_mongo/templates/"]
exclude = ["single_kernel_mongo/charmcraft.yaml"]
version = "0.0.0"

[tool.poetry.requires-plugins]
poetry-plugin-export = ">=1.8"
poetry-dynamic-versioning = { version = ">=1.0.0,<2.0.0", extras = ["plugin"] }

[tool.poetry-dynamic-versioning]
enable = true
enable = false
vcs = "git"
dirty = true
strict = true
Expand Down Expand Up @@ -121,6 +124,7 @@ parameterized = "^0.9.0"
factory_boy = "*"
mongomock = "^4.2.0.post1"
pytest-mock = "*"
ops = {version = "*", extras = ["testing"]}

[tool.poetry.group.integration.dependencies]
coverage = {extras = ["toml"], version = "^7.5.0"}
Expand All @@ -141,6 +145,7 @@ boto3 = "^1.37.12"
mypy-boto3-s3 = "^1.37.0"
tomli = '*'
tomli-w = '*'
hvac = ">=2.4.0,<3.0.0"

[tool.ruff]
target-version = "py310"
Expand Down
9 changes: 9 additions & 0 deletions single_kernel_mongo/config/literals.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@ class VmUser(WorkloadUser[int]):
group: int = 0


@dataclass(frozen=True)
class RootUser(WorkloadUser[str]):
"""The system user for VM charms."""

user: str = "root"
group: str = "root"


CRON_FILE = Path("/etc/cron.d/mongodb")

SYSTEMD_MONGODB_OVERRIDE = Path("/etc/systemd/system/snap.charmed-mongodb.mongod.service.d")
Expand Down Expand Up @@ -116,3 +124,4 @@ class TrustStoreFiles(str, Enum):

PBM = "pbm.crt"
LDAP = "ldap.crt"
VAULT = "vault.crt"
16 changes: 16 additions & 0 deletions single_kernel_mongo/config/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ class THPConfig:
THP_CONFIG = THPConfig()


VAULT_AGENT_TEMPLATE: Traversable = TEMPLATE_DIRECTORY / "vault-agent.hcl.j2"


@dataclass(frozen=True)
class OverrideFile:
"""Dataclass for the systemd override."""
Expand Down Expand Up @@ -194,6 +197,19 @@ class PasswordManagementState(Enum):
NEED_PASSWORD_UPDATE = auto()


class VaultConfigurationState(Enum):
"""State that can be mapped to a status."""

INVALID_CONFIG = auto()
DISABLED = auto()
VAULT_INTEGRATED = auto()
VAULT_NOT_INTEGRATED = auto()
MISSING_DATA = auto()
VAULT_UNREACHABLE = auto()
VAULT_AGENT_FAILED = auto()
ACTIVE = auto()


@dataclass
class PasswordManagementContext:
"""Represents the current context of password management for internal users.
Expand Down
1 change: 1 addition & 0 deletions single_kernel_mongo/config/relations.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class ExternalRequirerRelations(str, Enum):
GCS_CREDENTIALS = "gcs-credentials"
LDAP = "ldap"
LDAP_CERT = "ldap-certificate-transfer"
VAULT = "vault-kv"


class ExternalProviderRelations(str, Enum):
Expand Down
56 changes: 56 additions & 0 deletions single_kernel_mongo/config/statuses.py
Original file line number Diff line number Diff line change
Expand Up @@ -722,3 +722,59 @@ class PasswordManagementStatuses(Enum):
message="Failed to update user passwords.",
action="Check logs.",
)


class VaultStatuses(Enum):
"""Vault statuses for encryption at rest."""

INVALID_CONFIG = StatusObject(
status="blocked",
message="The enable-encryption-at-rest config option is invalid.",
short_message="Wrong enable-encryption-at-rest config.",
check="Config validation failed.",
action="Revert config option enable-encryption-at-rest value.",
approved_critical_component=True,
)
VAULT_INTEGRATED = StatusObject(
status="blocked",
message="The vault-kv interface cannot be used with encryption at rest disabled.",
short_message="Invalid vault-kv relation.",
check="Encryption at rest is disabled.",
action="Remove vault integration",
)
VAULT_NOT_INTEGRATED = StatusObject(
status="blocked",
message="Must be integrated with vault to enable encryption at rest.",
short_message="Integrate with vault.",
check="Missing vault relation.",
action="Integrate with vault charm.",
approved_critical_component=True,
)
MISSING_DATA = StatusObject(
status="waiting",
message="Still waiting data from vault.",
check="Missing vault relation.",
)
VAULT_UNREACHABLE = StatusObject(
status="blocked",
message="Vault is unreachable.",
check="Approle login failed.",
action="Ensure vault is running and reachable.",
)
VAULT_AGENT_FAILED = StatusObject(
status="blocked",
message="Vault agent failed to start.",
check="Vault agent is not running.",
action="Check configuration of vault agent.",
)
ACTIVE = StatusObject(
status="active",
message="",
)

### Running status
VAULT_ROTATE_MASTER_KEY = StatusObject(
status="maintenance",
message="Master key rotation in progress.",
running="blocking",
)
5 changes: 5 additions & 0 deletions single_kernel_mongo/core/k8s_workload.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class KubernetesWorkload(WorkloadBase):
substrate = "k8s"
container: Container # We always have a container in a Kubernetes Workload
users = KubernetesUser()
command_user = KubernetesUser()

def __init__(self, role: CharmSpec, container: Container | None) -> None:
if not container:
Expand Down Expand Up @@ -139,6 +140,8 @@ def exec(
env: dict[str, str] | None = None,
working_dir: str | None = None,
input: str | None = None,
user: str | None = None,
group: str | None = None,
) -> str:
masked_cmd = mask_sensitive_information(command)
try:
Expand All @@ -148,6 +151,8 @@ def exec(
working_dir=working_dir,
combine_stderr=True,
stdin=input,
user=user,
group=group,
)
output, _ = process.wait_output()
return output
Expand Down
1 change: 1 addition & 0 deletions single_kernel_mongo/core/structured_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ class MongoDBCharmConfig(MongoConfigModel):
system_users: str | None = Field(default=None, alias="system-users")

role: SerializeLiteralAsStr[MongoDBRoles] = Field(default=MongoDBRoles.REPLICATION)
enable_encryption_at_rest: bool = Field(default=False, alias="enable-encryption-at-rest")

@field_validator("role", mode="before")
@classmethod
Expand Down
6 changes: 6 additions & 0 deletions single_kernel_mongo/core/vm_workload.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

from single_kernel_mongo.config.literals import (
CRON_FILE,
RootUser,
VmUser,
)
from single_kernel_mongo.config.models import SNAP_NAME, CharmSpec
Expand All @@ -39,6 +40,7 @@ class VMWorkload(WorkloadBase):
substrate = "vm"
container: None
users = VmUser()
command_user = RootUser()

def __init__(self, role: CharmSpec, container: Container | None) -> None:
super().__init__(role, container)
Expand Down Expand Up @@ -131,6 +133,8 @@ def exec(
env: Mapping[str, str] | None = None,
working_dir: str | None = None,
input: str | None = None,
user: str | None = None,
group: str | None = None,
) -> str:
try:
output = subprocess.check_output(
Expand All @@ -141,6 +145,8 @@ def exec(
env=env,
cwd=working_dir,
input=input,
user=user,
group=group,
)
logger.debug(f"{output=}")
return output
Expand Down
15 changes: 15 additions & 0 deletions single_kernel_mongo/core/workload.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,16 @@ def ldap_certificates_file(self) -> Path:
"""The LDAP certificates file path."""
return Path(f"{self.ldap_certificates_dir}/ldap.crt")

@property
def vault_cert(self) -> Path:
"""Vault configuration file path."""
return Path(f"{self.etc_path}/vault/vault_cert.pem")

@property
def vault_token_file_path(self) -> Path:
"""Vault configuration file path."""
return Path(f"{self.etc_path}/vault/vaultTokenFile")


class WorkloadBase(ABC): # pragma: nocover
"""The protocol for workloads.
Expand All @@ -147,6 +157,9 @@ class WorkloadBase(ABC): # pragma: nocover
layer_name: ClassVar[str]
container: Container | None
users: ClassVar[WorkloadUser]
# Used as a command runner for all the cases where we need the user that runs the mongodb calls
# This is root:root on VM and mongodb:mongodb on kubernetes
command_user: ClassVar[WorkloadUser]
bin_cmd: ClassVar[str]
env_var: ClassVar[str]
snap_param: ClassVar[str]
Expand Down Expand Up @@ -246,6 +259,8 @@ def exec(
env: dict[str, str] | None = None,
working_dir: str | None = None,
input: str | None = None,
user: str | None = None,
group: str | None = None,
) -> str:
"""Runs a command on the workload substrate."""
...
Expand Down
5 changes: 5 additions & 0 deletions single_kernel_mongo/events/lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
SetPasswordError,
UpgradeInProgressError,
WaitingForLeaderError,
WaitingForVaultError,
WorkloadNotReadyError,
WorkloadServiceError,
)
Expand Down Expand Up @@ -156,6 +157,10 @@ def on_start(self, event: StartEvent):
)
event.defer()
return
except WaitingForVaultError:
logger.info("Waiting for vault to be integrated.")
event.defer()
return
except Exception as e:
logger.error(f"Deferring because of {e.__class__.__name__} {e}")
self.dependent.state.statuses.add(
Expand Down
Loading
Loading