Skip to content

Secrets API#861

Merged
benhoyt merged 48 commits intocanonical:mainfrom
benhoyt:secrets
Dec 14, 2022
Merged

Secrets API#861
benhoyt merged 48 commits intocanonical:mainfrom
benhoyt:secrets

Conversation

@benhoyt
Copy link
Collaborator

@benhoyt benhoyt commented Nov 28, 2022

This PR implements the ops wrapper for Juju secrets (3.0.2+). See also the secrets spec and the API sketch.

The PR includes the functions for charm use (in ops/model.py), the test harness for secrets (in ops/testing.py), and units tests for each of those parts.

QA steps

I've written two test charms, database (the secret owner) and webapp (the secret consumer) that exercise the code. To deploy locally:

$ juju bootstrap microk8s
$ juju add-model t
$ git clone https://github.com/benhoyt/test-charms
$ cd database
$ charmcraft pack
$ juju deploy ./database_ubuntu-22.04-amd64.charm
$ cd ../webapp
$ charmcraft pack
$ juju deploy ./webapp_ubuntu-22.04-amd64.charm
$ juju integrate database webapp
$ watch --color juju status --color
...
Model  Controller          Cloud/Region        Version  SLA          Timestamp
t      microk8s-localhost  microk8s/localhost  3.0.3    unsupported  15:35:25+13:00

App       Version  Status  Scale  Charm     Channel  Rev  Address         Exposed  Message
database           active      1  database             0  10.152.183.254  no       relation-created: added new secret
webapp             active      1  webapp               0  10.152.183.119  no       relation-changed: would update web app's db secret

Unit         Workload  Agent  Address       Ports  Message
database/0*  active    idle   10.1.164.149         relation-created: added new secret
webapp/0*    active    idle   10.1.164.138         relation-changed: would update web app's db secret

Documentation changes

Pietro's documentation additions (I plan to update these for latest API design and harness in Jan 2023).

Based on canonical#840 with rework for
the new API design and simplifications.
@benhoyt benhoyt mentioned this pull request Nov 28, 2022
@benhoyt benhoyt mentioned this pull request Dec 5, 2022
5 tasks
Copy link

@hemanthnakkina hemanthnakkina left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor comment inline

"""
return self._label

def get_content(self, *, refresh: bool = False) -> Dict[str, str]:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a developer I'd rather be using secret.content and secret.info instead of calling secret.get_content() and secret.get_info(). And since refresh is discretionary to the user I believe it's fine to run something like

secret.refresh()
print(secret.content)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the revision concept seems core the the idea of secrets, I wouldn't abstract it too much to the user.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The very prescriptive "@property should not be used on things which do dynamic lookups" mantra probably applies to the design here, but I definitely agree that a plain property would look nicer.

It's harder, though. I'm not sure whether or not "individual" parts of .info or .content could be set anyway, which would mean a somewhat weird behavior where some attributes must be set as complete objects even if they can be accessed with granularity. It doesn't look like it.

I think we have to pick our poison without adding an entirely new class or guidewires which protect setting some parts but not others. For example, you could do secret_id = s.info.id. But you could not do the converse. For consistency, we probably to pick one or the other, and get_...(), while it feels less Pythonic in some ways, is also very clear about the fact that you're not going to be able to in reverse through a @property to set the value.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revision is only visible to the owner of a secret. consumers of a secret cannot see the 'revision' attribute, their only option is to stick with the current content or 'refresh' to new content, leaving the old behind. (they cannot, for example, ask for 'exactly revision 10 of the content). Hence why it is pretty hidden in the consumer objects.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely. I think the discussion is far more about "what feels like a good API from OF" than how it works from Juju itself, though.

That is, foo.get_bar().some_property just doesn't feel Pythonic. My point in "picking our poison" is that, regardless of the visibility of any given attribute, it would be just as jarring to make get_content() into:

@property
def content(...)

Which would make it feel natural to be able to say secret.content.foo = bar, and that won't do what users expect, so it's down to picking whether get_content().revision is an ok tradeoff (it probably is) or doing ~50 lines of additional work to SecretInfo to make all of the attributes into _attribute with @property getters which do not have setters, so foo = secret.content.bar works but secret.content.foo = bar does not. That's just throwing a different kind of exception, even if it's more Pythonic to write.

get_content().... is fine IMO.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the comments, folks. Yeah, I think there are enough problem with properties here, given that some properties are not settable, some are owner and some consumer, etc. So I'm going to stick with what we have. Not perfect, but clear about what you can set and when hook tool calls are made.

YEARLY = 'yearly'


class SecretInfo:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need a class for SecretInfo? Couldn't we simply call secret.id or secret.revision? it seems cleaner than secret.get_info().id

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It gives an easy factory with from_dict, if nothing else, but agreed that many of the properties could be added to Secret more transparently.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also separates out "what is visible to Owners" vs "what is visible to Consumers".

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the reason John gives is the main one: most of the items in SecretInfo are only available to secret owners.


def grant(self, relation: 'Relation', *, unit: Optional[Unit] = None):
"""Grant read access to this secret.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not a huge fan of the fact that the user has to manually pass the secret id over the relation data even though it's been granted. It feels redundant and mistake prone. Here we can use the example from the database test charm:

secret.grant(event.relation)
event.relation.data[self.app]["db_password_id"] = secret.id

Secrets could have their own standard formatting in relation data. Example: {secrets: [{'id': '1234', 'label': "db-password"}]}. When you run secret.grant(relation), it gets added to the relation data, when you run secret.revoke(relation)` it gets removed.

Here the problem I see is every charm developer having to reinvent the wheel in terms managing the relation content with secret id's.

Any thoughts?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree this is interesting, and may have been worth discussing when the secrets spec was being finalized. However, for better or worse this ship has sailed (and John's points below are helpful too).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would worry, as always when "just automagically put stuff in relation data" (especially as nested objects) about the sheer amount of churn and events which would get fired off from a change like that, especially with a number of Juju workers the same as GOMAXPROCS.

Every secret event being added to relation data as a default more or less means that every unit in that relation will get an event, and when the foo_relation_changed hook fires, how much logic it performs and how much time it takes is more or less unknowable to Juju/OF, which could lead to an "event storm" pretty easily.

There's a more or less middle ground here would could be split out into a separate issue on OF (and, realistically, probably closed, but at least there's a place for discussion about it) to extend the signature here into:

def grant(self, relation: 'Relation', *, unit: Optional[Unit] = None, databag_key: Optional[str] = None):

Then, at the bottom of the method, to:

if databag_key:
  if unit:
    relation[unit][databag_key] = secret.id
  else:
    relation[relation.app][databag_key] = secret_id

I probably don't think OF should be handling this kind of operation. It's trivial enough to implement, and OF doesn't deal with handling actual charm data. Sure, it's asking charm authors to re-invent the wheel, but it's also true that my charms and your charms and Ben's charms and John's charms and so on don't have the same mental model of what's in a relation databag, the keys each of us pick for coherency will not be the same, there's no real expectation of arbitrary re-use of secrets with predictive names between my charm and yours, etc.

Personally, if I were picking an "idealized" workflow for secrets and relations as an addition to the spec, it would be a way to avoid the relation databag at all, and simply have the containeragent fire an event directly at the unit/app which was just granted access anyway.

@jameinel
Copy link
Member

jameinel commented Dec 12, 2022 via email

@gruyaume
Copy link

It's true that a charm could have access to multiple secrets granted from other charms. And for this reason, labels and descriptions should also be part of what is passed over the relation data so that it's easy to parse. Do we have usecases where this approach wouldn't work?

If charm X is related to charm Y and Z and both provide secrets to X. It's possible (and probable) that both will place secret id's differently in the relation data and charm X has to know about the specialities of both. Making handling secrets standard across charms would reduce complexity and probably avoid bugs.

label = secret.label
self.charm.on.secret_remove.emit(secret_id, label, revision)

def trigger_secret_expiration(self, secret_id: str, revision: int, *,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method names that trigger events don't follow the same conventions. For example container_pebble_ready triggers pebble ready and trigger_secret_expiration triggers secret expiration. I understand we probably don't want to break container_pebble_ready here but it'd be great to standardize over a convention (ex. trigger_x means that x event is triggered).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, good spotting. I do like the trigger_ prefix here, and think that container_pebble_ready should probably have been trigger_pebble_ready or similar (it takes the container name as a parameter). Yeah, I don't think we should change container_pebble_ready, but I'll keep in mind to standardize on trigger_x from now on.

ops/testing.py Outdated
self._backend._calls.clear()
return calls

def add_model_secret(self, app_or_unit: AppUnitOrName, content: Dict[str, str]) -> str:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know you detailed the reason why the model in the method name but the fact that a same method name is present in 2 classes that are used in different context isn't a problem in my opinion.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not a problem as such, and the previous iteration of this PR had it named add_secret, but we had a comment explaining why this was not the same as Application.add_secret (implying "be careful"), so I think the distinct name is best just to avoid confusion.

"""
owner_name = _get_app_or_unit_name(app_or_unit)
model.Secret._validate_content(content)
return self._backend._secret_add(content, owner_name)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like a secret object could be returned to the user instead of having them manipulate the secret ID.

def test_1():
    secret = self.harness.add_secret(owner="remote-app-name", content={"a", "b"})
    secret.grant(receiver="app-under-test-name")
    secret.revoke()

This being said, I understand this situation is not specific to secrets and that relations follow the same approach. But I hold the same opinion for relations (it would also be cleaner to access relation data using relation.data instead of get_relation_data(id=123))

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I played with this while writing it, but I decided against it for two reasons:

  1. As you've noted, other similar harness calls like get_relation_data don't do this.
  2. It's confusing to have a testing.Secret object that's somewhat similar to the model.Secret class, but also different enough to be confusing (and the methods have slightly different parameters/types).

So overall I think this is a reasonable approach that fits the current methods.

YEARLY = 'yearly'


class SecretInfo:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It gives an easy factory with from_dict, if nothing else, but agreed that many of the properties could be added to Secret more transparently.

return id

@classmethod
def _validate_content(cls, content: Optional[Dict[str, str]]):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't feel right. I'm not sure that a method to validate should be throwing exceptions this way. I would expect that a _is_valid_secret or _validate_secret_content or _validate_content method would return a bool, and that we'd log whatever the problems are. Particularly given the use case here.

It's definitely one of those times where I wish that Python had a stronger typing story, but as a user, I would hope for some way via the API to ask "is this dict able to converted into a secret?" before calling a method and being forced to catch ValueError|TypeError.

If would be one thing if this returned a materialized object, but the usage pattern seems to be "dict -> _validate_content -> no exception -> constructor".

Pointedly, this is not even invoked from the constructor itself, and probably should be. Otherwise, there's not necessarily much to stop users from just s = Secret(....) and getting back an invalid, pretty unusable Secret object.

Whether or not we add something like:

@classmethod
def is_valid(cls, content: Optional[Dict[str, str]]) -> bool:
    try:
        cls._validate_content(cls, content)
        return True
     except (ValueError, TypeError) as e:
        logger.debug("Invalid secret: {!r}", e.msg)
        return False

We should at least also invoke _validate_content from the constructor here for a guardrail.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kinda disagree here that this is an issue. For a start, if _validate_content (or whatever we call it) returns a bool, you can't give good error messages when you do raise.

I don't think we need a way for the user to ask is this a valid secret. You can just add it and the OF will check for you and you'll get an appropriate TypeError or ValueError if it's no good. That said, we can always add a user-facing function later if there's a compelling use case.

I don't think it needs to be invoked from the constructor, as "This class should not be instantiated directly" as the docstring says. The Secret class is difficult to instantiate directly anyway, as the first argument is backend, which the user doesn't really have access to (backend implementations and accessors are underscore-private). So the user won't be calling Secret() directly, and every place we call it with content we validate the content first. We could do it in the constructor as well, but that would duplicate effort.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is putting way too much faith into users reading the docs and/or using an editor with LSP hinting.

Even if we don't make a public method which allows users to validate "is this a valid dict to turn into a secret" which returns a bool (and logs messages along with returning False if it's not), this sort of validation should absolutely be invoked from the constructor.

It is not hard to instantiate directly. Python doesn't really enforce this, the constructor doesn't check, and it's trivial for users to just pass None, even if they only do it once, to say nothing of the fact that even __foo is still accessible from people writing code. As a user of OF, I don't want yet another place in my code where I need to potentially catch exceptions which OF should be handling (and re-raising as a more specific exception, like ModelError or whatever), and I definitely do not want to do it for a case like this.

Frankly, the only reason it's duplicating effort to put it into the constructor is because OF itself explicitly calls it before add_secret, and in set_content, this gets called, but there also, it checks the dict against an arbitrary, private, class-level regexp _key_re = re.compile(r'^([a-z](?:-?[a-z0-9]){2,})$'), and if it fails, OF throws a ValueError or TypeError. It's very, very possible for a relatively new user of Secrets to run into multiple exceptions (trying to pass a non-str as a value, trying a disallowed key format, etc) before actually encountering a usable value.

It's just a bad user experience. It is extremely safe, and easy, to both move the validation into the constructor itself instead of calling it from OF code before constructing an object (and logically identical). It's also easy to throw a InvalidSecretError with a message which includes the complete allowed format before users hit one or the other, and "the docs are clear" and/or "the Juju docs specify the format" probably aren't discoverable enough when it's this simple to make it better.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm ... I don't mind improving the experience here, but two things:

  1. By itself this doesn't make it good, but there's a precedent for this, for example Relation and RelationData and other model classes say the same thing, "This class should not be instantiated directly", but don't prevent you from instantiating it with a dummy backend and/or invalid data attributes if you really want (Python's "we're all consenting adults" philosophy).
  2. In practice, it seems to me that if you were blindly trying to call Secret() to instantiate a secret object, you'd at least have to get the args right, and your editor would show you that "backend" is a required argument. So you'd be guided to think "What do I need to supply for 'backend'? I'll read the docstring ... oh, I see, I should be calling add_secret or similar."

Has the issue you're describing been (or will this be) a problem in practice? Have we run into times where people are actually trying to instantiate (say) a Relation directly, with an explicit None backend, and things going wrong?

If we consider improving this we should at least know whether it's a real problem today, and then consider it on the larger scale of also improving the situation for the existing classes like Relation (in a separate PR).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, one other thing: it's actually logically different to call it in the constructor, because currently for example in add_secret we want to call it before calling the hook tool, whereas the Secret(...) call is only called after that. So if we added it to the constructor you'd still need it in both places.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. I mean, yes, it's Python, so you can do a lot of things. Just because it may be one way doesn't mean that it still can't be as safeguarded as possible, though. It only appears to be used in a few places.
  • [Application|Unit].add_secret (in both cases moving to the constructor is an easy step and logically identical).
  • Secret.set_content (in which case the validation could raise a more explicit exception with a clear message). It's already past the constructor here.
  • My suggestion that "we could call it from the constructor" doesn't necessarily mean "move the code to the constructor". Just to call the method from inside the constructor explicitly rather than before it in [Application|Unit].add_secret, which is more or less -2 lines (in each add_secret) and +1 (in __init__). It's pretty small and adds a lot of "training wheels"
  1. In practice, yes, sort-of. The assumption that users actually read and/or pay attention to what their editor suggests in dynamically-typed languages is generous, in my experience, but even so, it would be easy for OF users to run face-first into this when trying to set_content if they violated an invisible regexp (where the docstring is not nearly as clear as the messages in the exceptions raised by _validate_content) in any case.

Yes, we have run into people trying to instantiate, say, a Relation directly, albeit mostly people who are new to charming and/or OF.

More directly, we (and I) have been impacted by needing to add an annoyingly high amount of exception guarding in charm code from behavior changes in OF (in particular, relation_get suddenly being able to return None). Open source is a democracy/consensus model, and sometimes pain points are deemed to be too much, and sometimes they're acceptable consequences. I mean, you don't necessarily have to implement the suggestions here. I'm not always right, but I always have an opinion ;)

My opinion in this case is that the amount of work needed to "bubble wrap" things a little bit and provide more meaningful exceptions is small in terms of the potential time saved for charm authors, but "considered, and I'm not going to, they can read the docs" is ok, too. It's your call, ultimately.

In practice, _key_re requires _validate_content to be a @classmethod to access it, but it's never accessed by anything else, and could just as easily be a top-level constant if we really wanted to compile it only once, or a method-level variable anyway, in which case _validate_content could also be @staticmethod anyway.

The desire to call it prior to the hook is true, but the constructor for Secret has id as optional anyway. Again, in practice, there's not a practical difference between instantiating Secret first and setting Secret._id after the hook tool versus the converse except for your preference, and that doing the inverse adds input validation to the constructor. Your choice, in the end.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding "logically identical", maybe I'm missing what you're saying here, but moving the _validate_content step to the constructor wouldn't work for the two add_secret methods, because you need to validate before calling the hook tool. I could create the Secret object at the top of those functions, but like you suggest, you'd have to poke the id in afterwards, so it's awkward. And for get_secret it would work, but mean we're unnecessary validating content we just received from Juju.

More directly, we (and I) have been impacted by needing to add an annoyingly high amount of exception guarding in charm code from behavior changes in OF (in particular, relation_get suddenly being able to return None).

This is a fair point in general. I'm hoping to be very careful with backwards compatibility as I shepherd ops. Even for the upcoming 2.0 release, which is a major version bump and can be backwards-incompatible, we're planning to be extremely careful about changing stuff, and almost all charms should continue to work as is. The main "breakage" is not API at all, but bumping the minimum Python version up to 3.8.

My opinion in this case is that the amount of work needed to "bubble wrap" things a little bit and provide more meaningful exceptions is small in terms of the potential time saved for charm authors, but "considered, and I'm not going to, they can read the docs" is ok, too. It's your call, ultimately.

It's a fair point of view. My take is that it's a bit of a false sense of security, and if people are just blindly providing dummy values for things like backend (not to mention ignoring docs), they need to up their coding game. :-)

Another counterpoint is that you can only go so far with more and more checks before the implementation (and even sometimes the usage) is unwieldy. In future we're planning to lean in harder on static type checking. But in the meantime we're striving for a happy medium of a few runtime checks as appropriate.

All that said, I've considered your point of view, but decided not to make this specific change now.

"""
return self._label

def get_content(self, *, refresh: bool = False) -> Dict[str, str]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The very prescriptive "@property should not be used on things which do dynamic lookups" mantra probably applies to the design here, but I definitely agree that a plain property would look nicer.

It's harder, though. I'm not sure whether or not "individual" parts of .info or .content could be set anyway, which would mean a somewhat weird behavior where some attributes must be set as complete objects even if they can be accessed with granularity. It doesn't look like it.

I think we have to pick our poison without adding an entirely new class or guidewires which protect setting some parts but not others. For example, you could do secret_id = s.info.id. But you could not do the converse. For consistency, we probably to pick one or the other, and get_...(), while it feels less Pythonic in some ways, is also very clear about the fact that you're not going to be able to in reverse through a @property to set the value.


def grant(self, relation: 'Relation', *, unit: Optional[Unit] = None):
"""Grant read access to this secret.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would worry, as always when "just automagically put stuff in relation data" (especially as nested objects) about the sheer amount of churn and events which would get fired off from a change like that, especially with a number of Juju workers the same as GOMAXPROCS.

Every secret event being added to relation data as a default more or less means that every unit in that relation will get an event, and when the foo_relation_changed hook fires, how much logic it performs and how much time it takes is more or less unknowable to Juju/OF, which could lead to an "event storm" pretty easily.

There's a more or less middle ground here would could be split out into a separate issue on OF (and, realistically, probably closed, but at least there's a place for discussion about it) to extend the signature here into:

def grant(self, relation: 'Relation', *, unit: Optional[Unit] = None, databag_key: Optional[str] = None):

Then, at the bottom of the method, to:

if databag_key:
  if unit:
    relation[unit][databag_key] = secret.id
  else:
    relation[relation.app][databag_key] = secret_id

I probably don't think OF should be handling this kind of operation. It's trivial enough to implement, and OF doesn't deal with handling actual charm data. Sure, it's asking charm authors to re-invent the wheel, but it's also true that my charms and your charms and Ben's charms and John's charms and so on don't have the same mental model of what's in a relation databag, the keys each of us pick for coherency will not be the same, there's no real expectation of arbitrary re-use of secrets with predictive names between my charm and yours, etc.

Personally, if I were picking an "idealized" workflow for secrets and relations as an addition to the spec, it would be a way to avoid the relation databag at all, and simply have the containeragent fire an event directly at the unit/app which was just granted access anyway.

@jameinel
Copy link
Member

It's true that a charm could have access to multiple secrets granted from other charms. And for this reason, labels and descriptions should also be part of what is passed over the relation data so that it's easy to parse. Do we have usecases where this approach wouldn't work?

If charm X is related to charm Y and Z and both provide secrets to X. It's possible (and probable) that both will place secret id's differently in the relation data and charm X has to know about the specialities of both. Making handling secrets standard across charms would reduce complexity and probably avoid bugs.

The issue for labels and description is that the label is my reference for this object not my reference for you to understand this object. And description is meant to be the human readable message as to why this content exists, which again is not a good means of communication from application to application.
Note that we don't have the equivalent metadata of 'endpoint name' for a given application to track the use cases for a secret it is generating, so the only thing we have is 'label'. I'd like to keep that as the "handle for my application against this secret", and not have to worry about "handle for my application and how I'm sharing it with other applications". Which means we need some sort of handle for the latter. And we could create another attribute of the secret, but we do already have relation key.
And absolutely, a given relation may have no similarity to another relation. But that's already the case for relation data. Do you pass a URL or a postgresql set of keys for how you access the database. It is part of the protocol of relation interfaces that they define what keys should be available, and what names will they be accessed by.

I agree that we could do a way of naming secrets in a uniform fashion as you grant them to the other side of a relation. But we have relation keys that handle 90% of that already, in exchange for occasionally needing to be aware of the secret's id.

Copy link
Member

@jameinel jameinel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the rest of the polish that we might wan to apply can certainly happen iteratively. I think overall this looks quite good.


def __init__(self, handle: 'Handle', id: str, label: Optional[str], revision: int):
super().__init__(handle, id, label)
self._revision = revision
Copy link
Member

Choose a reason for hiding this comment

The 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" ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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 .revision. Hmmm, going to punt for now (can always tweak later).

Starting an already-started service now succeeds (start is idempotent).
So just remove this comment.

cannot perform the following tasks:
- Start service "test" (service "test" was previously started)
"""
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drive by fix: Starting an already-started service now succeeds (start is idempotent). So just remove this comment.

Copy link
Member

@jnsgruk jnsgruk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work, thanks Ben!

@benhoyt benhoyt merged commit 28d8157 into canonical:main Dec 14, 2022
@benhoyt benhoyt deleted the secrets branch January 5, 2023 03:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants