-
Notifications
You must be signed in to change notification settings - Fork 129
Secrets API #861
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
Secrets API #861
Changes from all commits
772862b
fb5deb8
937bf4a
7756a8d
95cdf7c
87934fb
b33e949
7541d59
7767b47
4c8e656
f706db0
30ece7d
8cc6d3f
1b19651
a382a98
86ed899
5ce4a40
ae434e0
998114a
59a0273
c4d4138
5fd149f
720518d
7d97a97
665bc7c
f1ae48f
6647e35
77ee815
a2fb674
3e0b838
c9df5ee
4f29530
11acdf3
a00ad75
e983af5
d83b200
726f032
72f9335
f95e81f
6e67361
3e981b3
cf2352e
50a0131
30736a9
fe9a181
f18db12
a8d089c
4f7e101
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| # Copyright 2022 Canonical Ltd. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| """Time conversion utilities.""" | ||
|
|
||
| import datetime | ||
| import re | ||
|
|
||
| # Matches yyyy-mm-ddTHH:MM:SS(.sss)ZZZ | ||
| _TIMESTAMP_RE = re.compile( | ||
| r'(\d{4})-(\d{2})-(\d{2})[Tt](\d{2}):(\d{2}):(\d{2})(\.\d+)?(.*)') | ||
benhoyt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| # Matches [-+]HH:MM | ||
| _TIMEOFFSET_RE = re.compile(r'([-+])(\d{2}):(\d{2})') | ||
|
|
||
|
|
||
| def parse_rfc3339(s: str) -> datetime.datetime: | ||
| """Parse an RFC3339 timestamp. | ||
|
|
||
| This parses RFC3339 timestamps (which are a subset of ISO8601 timestamps) | ||
| that Go's encoding/json package produces for time.Time values. | ||
|
|
||
| Unfortunately we can't use datetime.fromisoformat(), as that does not | ||
| support more than 6 digits for the fractional second, nor the 'Z' for UTC. | ||
| """ | ||
| match = _TIMESTAMP_RE.match(s) | ||
| if not match: | ||
| raise ValueError('invalid timestamp {!r}'.format(s)) | ||
| y, m, d, hh, mm, ss, sfrac, zone = match.groups() | ||
|
|
||
| if zone in ('Z', 'z'): | ||
| tz = datetime.timezone.utc | ||
| else: | ||
| match = _TIMEOFFSET_RE.match(zone) | ||
| if not match: | ||
| raise ValueError('invalid timestamp {!r}'.format(s)) | ||
| sign, zh, zm = match.groups() | ||
| tz_delta = datetime.timedelta(hours=int(zh), minutes=int(zm)) | ||
| tz = datetime.timezone(tz_delta if sign == '+' else -tz_delta) | ||
|
|
||
| microsecond = round(float(sfrac or '0') * 1000000) | ||
|
|
||
| return datetime.datetime(int(y), int(m), int(d), int(hh), int(mm), int(ss), | ||
| microsecond=microsecond, tzinfo=tz) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -286,7 +286,7 @@ class ConfigChangedEvent(HookEvent): | |
| rescheduling, on unit upgrade/refresh, etc... | ||
| - As a specific instance of the above point: when networking changes | ||
| (if the machine reboots and comes up with a different IP). | ||
| - When the cloud admin reconfigures the charm via the juju CLI, i.e. | ||
| - When the cloud admin reconfigures the charm via the Juju CLI, i.e. | ||
| `juju config my-charm foo=bar`. This event notifies the charm of | ||
| its new configuration. (The event itself, however, is not aware of *what* | ||
| specifically has changed in the config). | ||
|
|
@@ -742,6 +742,143 @@ class PebbleReadyEvent(WorkloadEvent): | |
| """ | ||
|
|
||
|
|
||
| class SecretEvent(HookEvent): | ||
| """Base class for all secret events.""" | ||
|
|
||
| def __init__(self, handle: 'Handle', id: str, label: Optional[str]): | ||
| super().__init__(handle) | ||
| self._id = id | ||
| self._label = label | ||
|
|
||
| @property | ||
| def secret(self) -> model.Secret: | ||
| """The secret instance this event refers to.""" | ||
| backend = self.framework.model._backend | ||
| return model.Secret(backend=backend, id=self._id, label=self._label) | ||
|
|
||
| def snapshot(self) -> '_SerializedData': | ||
| """Used by the framework to serialize the event to disk. | ||
|
|
||
| Not meant to be called by charm code. | ||
| """ | ||
| return {'id': self._id, 'label': self._label} | ||
|
|
||
| def restore(self, snapshot: '_SerializedData'): | ||
| """Used by the framework to deserialize the event from disk. | ||
|
|
||
| Not meant to be called by charm code. | ||
| """ | ||
| self._id = cast(str, snapshot['id']) | ||
| self._label = cast(Optional[str], snapshot['label']) | ||
|
|
||
|
|
||
| class SecretChangedEvent(SecretEvent): | ||
| """Event raised by Juju on the observer when the secret owner changes its contents. | ||
|
|
||
| When the owner of a secret changes the secret's contents, Juju will create | ||
| a new secret revision, and all applications or units that are tracking this | ||
| secret will be notified via this event that a new revision is available. | ||
|
|
||
| Typically, you will want to fetch the new content by calling | ||
| :meth:`ops.model.Secret.get_content` with :code:`refresh=True` to tell Juju to | ||
| start tracking the new revision. | ||
| """ | ||
|
|
||
|
|
||
| class SecretRotateEvent(SecretEvent): | ||
| """Event raised by Juju on the owner when the secret's rotation policy elapses. | ||
|
|
||
| This event is fired on the secret owner to inform it that the secret must | ||
| be rotated. The event will keep firing until the owner creates a new | ||
| revision by calling :meth:`ops.model.Secret.set_content`. | ||
| """ | ||
|
|
||
| def defer(self): | ||
| """Secret rotation events are not deferrable (Juju handles re-invocation).""" | ||
| raise RuntimeError( | ||
| 'Cannot defer secret rotation events. Juju will keep firing this ' | ||
| 'event until you create a new revision.') | ||
|
|
||
|
|
||
| class SecretRemoveEvent(SecretEvent): | ||
| """Event raised by Juju on the owner when a secret revision can be removed. | ||
|
|
||
| When the owner of a secret creates a new revision, and after all | ||
| observers have updated to that new revision, this event will be fired to | ||
| inform the secret owner that the old revision can be removed. | ||
|
|
||
| Typically, you will want to call :meth:`ops.model.Secret.remove_revision` to | ||
| remove the now-unused revision. | ||
| """ | ||
|
|
||
| def __init__(self, handle: 'Handle', id: str, label: Optional[str], revision: int): | ||
| super().__init__(handle, id, label) | ||
| self._revision = revision | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since the next three all just add _revision, should they have a common "SecretOwnerEvent" or "SecretRevisionEvent" ?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I actually had SecretOwnerEvent previously, but it's actually not all owner events (SecretRotateEvent doesn't have a revision). I tried adding a _SecretEventWithRevision just now, and it works, but doesn't show up nicely in the docs: I don't want that class part of the public API, but then the docs generator doesn't show it at all and you can't see that those two subclasses have a |
||
|
|
||
| @property | ||
| def revision(self) -> int: | ||
| """The secret revision this event refers to.""" | ||
| return self._revision | ||
|
|
||
| def snapshot(self) -> '_SerializedData': | ||
| """Used by the framework to serialize the event to disk. | ||
|
|
||
| Not meant to be called by charm code. | ||
| """ | ||
| data = super().snapshot() | ||
benhoyt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| data['revision'] = self._revision | ||
| return data | ||
|
|
||
| def restore(self, snapshot: '_SerializedData'): | ||
| """Used by the framework to deserialize the event from disk. | ||
|
|
||
| Not meant to be called by charm code. | ||
| """ | ||
| super().restore(snapshot) | ||
| self._revision = cast(int, snapshot['revision']) | ||
|
|
||
|
|
||
| class SecretExpiredEvent(SecretEvent): | ||
| """Event raised by Juju on the owner when a secret's expiration time elapses. | ||
|
|
||
| This event is fired on the secret owner to inform it that the secret revision | ||
| must be removed. The event will keep firing until the owner removes the | ||
| revision by calling :meth:`model.Secret.remove_revision()`. | ||
| """ | ||
|
|
||
| def __init__(self, handle: 'Handle', id: str, label: Optional[str], revision: int): | ||
| super().__init__(handle, id, label) | ||
| self._revision = revision | ||
|
|
||
| @property | ||
| def revision(self) -> int: | ||
| """The secret revision this event refers to.""" | ||
| return self._revision | ||
|
|
||
| def snapshot(self) -> '_SerializedData': | ||
| """Used by the framework to serialize the event to disk. | ||
|
|
||
| Not meant to be called by charm code. | ||
| """ | ||
| data = super().snapshot() | ||
| data['revision'] = self._revision | ||
| return data | ||
|
|
||
| def restore(self, snapshot: '_SerializedData'): | ||
| """Used by the framework to deserialize the event from disk. | ||
|
|
||
| Not meant to be called by charm code. | ||
| """ | ||
| super().restore(snapshot) | ||
| self._revision = cast(int, snapshot['revision']) | ||
|
|
||
| def defer(self): | ||
| """Secret expiration events are not deferrable (Juju handles re-invocation).""" | ||
| raise RuntimeError( | ||
| 'Cannot defer secret expiration events. Juju will keep firing ' | ||
| 'this event until you create a new revision.') | ||
|
|
||
|
|
||
| class CharmEvents(ObjectEvents): | ||
| """Events generated by Juju pertaining to application lifecycle. | ||
|
|
||
|
|
@@ -783,6 +920,11 @@ class CharmEvents(ObjectEvents): | |
| leader_settings_changed = EventSource(LeaderSettingsChangedEvent) | ||
| collect_metrics = EventSource(CollectMetricsEvent) | ||
|
|
||
| secret_changed = EventSource(SecretChangedEvent) | ||
| secret_expired = EventSource(SecretExpiredEvent) | ||
| secret_rotate = EventSource(SecretRotateEvent) | ||
| secret_remove = EventSource(SecretRemoveEvent) | ||
|
|
||
|
|
||
| class CharmBase(Object): | ||
| """Base class that represents the charm overall. | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.