Skip to content

Commit

Permalink
[DPE-2731] Secret labels (6/edge) (#270)
Browse files Browse the repository at this point in the history
  • Loading branch information
juditnovak authored Oct 17, 2023
1 parent b19fdb6 commit 4345604
Show file tree
Hide file tree
Showing 9 changed files with 460 additions and 158 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ jobs:
uses: actions/download-artifact@v3
with:
name: ${{ needs.build.outputs.artifact-name }}
- name: Free up disk space
run: |
# From https://github.com/actions/runner-images/issues/2840#issuecomment-790492173
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
sudo rm -rf /usr/local/share/boost
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
- name: Select tests
id: select-tests
run: |
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
6 changes: 3 additions & 3 deletions lib/charms/mongodb/v1/mongodb_backups.py
Original file line number Diff line number Diff line change
Expand Up @@ -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) is subprocess.CalledProcessError:
if isinstance(e, subprocess.CalledProcessError):
error_message = e.output.decode("utf-8")
else:
error_message = str(e.stderr)
Expand Down Expand Up @@ -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) is subprocess.CalledProcessError:
if isinstance(e, subprocess.CalledProcessError):
error_message = e.output.decode("utf-8")
else:
error_message = str(e.stderr)
Expand Down Expand Up @@ -642,7 +642,7 @@ def _get_backup_restore_operation_result(self, current_pbm_status, previous_pbm_
return f"Operation is still in progress: '{current_pbm_status.message}'"

if (
type(previous_pbm_status) is 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
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

0 comments on commit 4345604

Please sign in to comment.