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

[DPE-2731] Secret labels + missing unittests #268

Closed
wants to merge 5 commits into from
Closed
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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Testing high availability on a production cluster can be done with:
tox run -e ha-integration -- --model=<model_name>
```

Note if you'd like to test storage re-use in ha-testing, your storage must not be of the type `rootfs`. `rootfs` storage is tied to the machine lifecycle and does not stick around after unit removal. `rootfs` storage is used by default with `tox run -e ha-integration`. To test ha-testing for storage re-use:
Note if you'd like to test storage reuse in ha-testing, your storage must not be of the type `rootfs`. `rootfs` storage is tied to the machine lifecycle and does not stick around after unit removal. `rootfs` storage is used by default with `tox run -e ha-integration`. To test ha-testing for storage reuse:
```shell
juju create-storage-pool mongodb-ebs ebs volume-type=standard # create a storage pool
juju deploy ./*charm --storage mongodb=mongodb-ebs,7G,1 # deploy 1 or more units of application with said storage pool
Expand Down
2 changes: 1 addition & 1 deletion lib/charms/mongodb/v0/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ def process_pbm_error(error_string: Optional[_StrOrBytes]) -> str:
message = "couldn't configure s3 backup option"
if not error_string:
return message
if type(error_string) == bytes:
if isinstance(error_string, bytes):
error_string = error_string.decode("utf-8")
if "status code: 403" in error_string: # type: ignore
message = "s3 credentials are incorrect."
Expand Down
2 changes: 1 addition & 1 deletion lib/charms/mongodb/v0/mongodb.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ def create_role(self, role_name: str, privileges: dict, roles: dict = []):

Args:
role_name: name of the role to be added.
privileges: privledges to be associated with the role.
privileges: privileges to be associated with the role.
roles: List of roles from which this role inherits privileges.
"""
try:
Expand Down
12 changes: 6 additions & 6 deletions lib/charms/mongodb/v0/mongodb_backups.py
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,7 @@ def _try_to_restore(self, backup_id: str) -> None:

If PBM is resyncing, the function will retry to create backup
(up to BACKUP_RESTORE_MAX_ATTEMPTS times) with BACKUP_RESTORE_ATTEMPT_COOLDOWN
time between attepts.
time between attempts.

If PMB returen any other error, the function will raise RestoreError.
"""
Expand All @@ -523,7 +523,7 @@ def _try_to_restore(self, backup_id: str) -> None:
restore_cmd = restore_cmd + remapping_args.split(" ")
self.charm.run_pbm_command(restore_cmd)
except (subprocess.CalledProcessError, ExecError) as e:
if type(e) == subprocess.CalledProcessError:
if isinstance(e, subprocess.CalledProcessError):
error_message = e.output.decode("utf-8")
else:
error_message = str(e.stderr)
Expand All @@ -541,7 +541,7 @@ def _try_to_backup(self):

If PBM is resyncing, the function will retry to create backup
(up to BACKUP_RESTORE_MAX_ATTEMPTS times)
with BACKUP_RESTORE_ATTEMPT_COOLDOWN time between attepts.
with BACKUP_RESTORE_ATTEMPT_COOLDOWN time between attempts.

If PMB returen any other error, the function will raise BackupError.
"""
Expand All @@ -560,7 +560,7 @@ def _try_to_backup(self):
)
return backup_id_match.group("backup_id") if backup_id_match else "N/A"
except (subprocess.CalledProcessError, ExecError) as e:
if type(e) == subprocess.CalledProcessError:
if isinstance(e, subprocess.CalledProcessError):
error_message = e.output.decode("utf-8")
else:
error_message = str(e.stderr)
Expand Down Expand Up @@ -636,13 +636,13 @@ def _get_backup_restore_operation_result(self, current_pbm_status, previous_pbm_
to contain the operation type (backup/restore) and the backup id.
"""
if (
type(current_pbm_status) == type(previous_pbm_status)
type(current_pbm_status) is type(previous_pbm_status)
and current_pbm_status.message == previous_pbm_status.message
):
return f"Operation is still in progress: '{current_pbm_status.message}'"

if (
type(previous_pbm_status) == MaintenanceStatus
isinstance(previous_pbm_status, MaintenanceStatus)
and "backup id:" in previous_pbm_status.message
):
backup_id = previous_pbm_status.message.split("backup id:")[-1].strip()
Expand Down
137 changes: 137 additions & 0 deletions lib/charms/mongodb/v0/mongodb_secrets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
"""Secrets related helper classes/functions."""
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.

from typing import Dict, Optional

from ops import Secret, SecretInfo
from ops.charm import CharmBase
from ops.model import SecretNotFoundError

from config import Config
from exceptions import SecretAlreadyExistsError

# The unique Charmhub library identifier, never change it

# The unique Charmhub library identifier, never change it
LIBID = "87456e41c7594240b92b783a648592b5"

# Increment this major API version when introducing breaking changes
LIBAPI = 0

# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 1

APP_SCOPE = Config.Relations.APP_SCOPE
UNIT_SCOPE = Config.Relations.UNIT_SCOPE
Scopes = Config.Relations.Scopes


def generate_secret_label(charm: CharmBase, scope: Scopes) -> str:
"""Generate unique group_mappings for secrets within a relation context.

Defined as a standalone function, as the choice on secret labels definition belongs to the
Application Logic. To be kept separate from classes below, which are simply to provide a
(smart) abstraction layer above Juju Secrets.
"""
members = [charm.app.name, scope]
return f"{'.'.join(members)}"


# Secret cache


class CachedSecret:
"""Abstraction layer above direct Juju access with caching.

The data structure is precisely re-using/simulating Juju Secrets behavior, while
also making sure not to fetch a secret multiple times within the same event scope.
"""

def __init__(self, charm: CharmBase, label: str, secret_uri: Optional[str] = None):
self._secret_meta = None
self._secret_content = {}
self._secret_uri = secret_uri
self.label = label
self.charm = charm

def add_secret(self, content: Dict[str, str], scope: Scopes) -> Secret:
"""Create a new secret."""
if self._secret_uri:
raise SecretAlreadyExistsError(
"Secret is already defined with uri %s", self._secret_uri
)

if scope == Config.Relations.APP_SCOPE:
secret = self.charm.app.add_secret(content, label=self.label)
else:
secret = self.charm.unit.add_secret(content, label=self.label)
self._secret_uri = secret.id
self._secret_meta = secret
return self._secret_meta

@property
def meta(self) -> Optional[Secret]:
"""Getting cached secret meta-information."""
if self._secret_meta:
return self._secret_meta

if not (self._secret_uri or self.label):
return

try:
self._secret_meta = self.charm.model.get_secret(label=self.label)
except SecretNotFoundError:
if self._secret_uri:
self._secret_meta = self.charm.model.get_secret(
id=self._secret_uri, label=self.label
)
return self._secret_meta

def get_content(self) -> Dict[str, str]:
"""Getting cached secret content."""
if not self._secret_content:
if self.meta:
self._secret_content = self.meta.get_content()
return self._secret_content

def set_content(self, content: Dict[str, str]) -> None:
"""Setting cached secret content."""
if self.meta:
self.meta.set_content(content)
self._secret_content = content

def get_info(self) -> Optional[SecretInfo]:
"""Wrapper function for get the corresponding call on the Secret object if any."""
if self.meta:
return self.meta.get_info()


class SecretCache:
"""A data structure storing CachedSecret objects."""

def __init__(self, charm):
self.charm = charm
self._secrets: Dict[str, CachedSecret] = {}

def get(self, label: str, uri: Optional[str] = None) -> Optional[CachedSecret]:
"""Getting a secret from Juju Secret store or cache."""
if not self._secrets.get(label):
secret = CachedSecret(self.charm, label, uri)
if secret.meta:
self._secrets[label] = secret
return self._secrets.get(label)

def add(self, label: str, content: Dict[str, str], scope: Scopes) -> CachedSecret:
"""Adding a secret to Juju Secret."""
if self._secrets.get(label):
raise SecretAlreadyExistsError(f"Secret {label} already exists")

secret = CachedSecret(self.charm, label)
secret.add_secret(content, scope)
self._secrets[label] = secret
return self._secrets[label]


# END: Secret cache
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ pyrsistent==0.19.3
pyyaml==6.0.1
zipp==3.11.0
pyOpenSSL==22.1.0
typing-extensions==4.5.0
typing-extensions==4.5.0
parameterized==0.9.0
Loading
Loading