Skip to content

Commit 943e4bf

Browse files
authored
[DPE-2838] Secret labels (#270)
* Secret labels * Missing arg * Pop the right databag * Copy over safe secret get
1 parent 4ab0f51 commit 943e4bf

File tree

5 files changed

+345
-243
lines changed

5 files changed

+345
-243
lines changed
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
"""Secrets related helper classes/functions."""
2+
# Copyright 2023 Canonical Ltd.
3+
# See LICENSE file for licensing details.
4+
5+
from typing import Dict, Literal, Optional
6+
7+
from ops import Secret, SecretInfo
8+
from ops.charm import CharmBase
9+
from ops.model import SecretNotFoundError
10+
11+
# The unique Charmhub library identifier, never change it
12+
LIBID = "d77fb3d01aba41ed88e837d0beab6be5"
13+
14+
# Increment this major API version when introducing breaking changes
15+
LIBAPI = 0
16+
17+
# Increment this PATCH version before using `charmcraft publish-lib` or reset
18+
# to 0 if you are raising the major API version
19+
LIBPATCH = 1
20+
21+
22+
APP_SCOPE = "app"
23+
UNIT_SCOPE = "unit"
24+
Scopes = Literal["app", "unit"]
25+
26+
27+
class DataSecretsError(Exception):
28+
"""A secret that we want to create already exists."""
29+
30+
31+
class SecretAlreadyExistsError(DataSecretsError):
32+
"""A secret that we want to create already exists."""
33+
34+
35+
def generate_secret_label(charm: CharmBase, scope: Scopes) -> str:
36+
"""Generate unique group_mappings for secrets within a relation context.
37+
38+
Defined as a standalone function, as the choice on secret labels definition belongs to the
39+
Application Logic. To be kept separate from classes below, which are simply to provide a
40+
(smart) abstraction layer above Juju Secrets.
41+
"""
42+
members = [charm.app.name, scope]
43+
return f"{'.'.join(members)}"
44+
45+
46+
# Secret cache
47+
48+
49+
class CachedSecret:
50+
"""Abstraction layer above direct Juju access with caching.
51+
52+
The data structure is precisely re-using/simulating Juju Secrets behavior, while
53+
also making sure not to fetch a secret multiple times within the same event scope.
54+
"""
55+
56+
def __init__(self, charm: CharmBase, label: str, secret_uri: Optional[str] = None):
57+
self._secret_meta = None
58+
self._secret_content = {}
59+
self._secret_uri = secret_uri
60+
self.label = label
61+
self.charm = charm
62+
63+
def add_secret(self, content: Dict[str, str], scope: Scopes) -> Secret:
64+
"""Create a new secret."""
65+
if self._secret_uri:
66+
raise SecretAlreadyExistsError(
67+
"Secret is already defined with uri %s", self._secret_uri
68+
)
69+
70+
if scope == APP_SCOPE:
71+
secret = self.charm.app.add_secret(content, label=self.label)
72+
else:
73+
secret = self.charm.unit.add_secret(content, label=self.label)
74+
self._secret_uri = secret.id
75+
self._secret_meta = secret
76+
return self._secret_meta
77+
78+
@property
79+
def meta(self) -> Optional[Secret]:
80+
"""Getting cached secret meta-information."""
81+
if self._secret_meta:
82+
return self._secret_meta
83+
84+
if not (self._secret_uri or self.label):
85+
return
86+
87+
try:
88+
self._secret_meta = self.charm.model.get_secret(label=self.label)
89+
except SecretNotFoundError:
90+
if self._secret_uri:
91+
self._secret_meta = self.charm.model.get_secret(
92+
id=self._secret_uri, label=self.label
93+
)
94+
return self._secret_meta
95+
96+
def get_content(self) -> Dict[str, str]:
97+
"""Getting cached secret content."""
98+
if not self._secret_content:
99+
if self.meta:
100+
self._secret_content = self.meta.get_content()
101+
return self._secret_content
102+
103+
def set_content(self, content: Dict[str, str]) -> None:
104+
"""Setting cached secret content."""
105+
if self.meta:
106+
self.meta.set_content(content)
107+
self._secret_content = content
108+
109+
def get_info(self) -> Optional[SecretInfo]:
110+
"""Wrapper function for get the corresponding call on the Secret object if any."""
111+
if self.meta:
112+
return self.meta.get_info()
113+
114+
115+
class SecretCache:
116+
"""A data structure storing CachedSecret objects."""
117+
118+
def __init__(self, charm):
119+
self.charm = charm
120+
self._secrets: Dict[str, CachedSecret] = {}
121+
122+
def get(self, label: str, uri: Optional[str] = None) -> Optional[CachedSecret]:
123+
"""Getting a secret from Juju Secret store or cache."""
124+
if not self._secrets.get(label):
125+
secret = CachedSecret(self.charm, label, uri)
126+
127+
# Checking if the secret exists, otherwise we don't register it in the cache
128+
if secret.meta:
129+
self._secrets[label] = secret
130+
return self._secrets.get(label)
131+
132+
def add(self, label: str, content: Dict[str, str], scope: Scopes) -> CachedSecret:
133+
"""Adding a secret to Juju Secret."""
134+
if self._secrets.get(label):
135+
raise SecretAlreadyExistsError(f"Secret {label} already exists")
136+
137+
secret = CachedSecret(self.charm, label)
138+
secret.add_secret(content, scope)
139+
self._secrets[label] = secret
140+
return self._secrets[label]
141+
142+
143+
# END: Secret cache

0 commit comments

Comments
 (0)