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

Cannot call Secret.set_info and Secret.set_content in the same hook #1288

Closed
tonyandrewmeyer opened this issue Jul 18, 2024 · 7 comments · Fixed by #1373
Closed

Cannot call Secret.set_info and Secret.set_content in the same hook #1288

tonyandrewmeyer opened this issue Jul 18, 2024 · 7 comments · Fixed by #1373
Assignees
Labels
24.10 bug Something isn't working

Comments

@tonyandrewmeyer
Copy link
Contributor

If Secret.set_info() and Secret.set_content() are both called in the same hook, then whichever is first is ignored.

I expect that from a Juju point of view, this is because secret_set has been called twice and so it assumes that the later call replaces the earlier one, rather than trying to merge the two together. This is particularly unexpected behaviour in ops because we have two different methods exposing parts of secret_set, so a charmer doesn't necessarily know that it's the same underlying Juju hook tool.

This can be reproduced with this small charm:

class SecretInfoAndContentSetCharm(ops.CharmBase):
    """Charm the application."""

    def __init__(self, framework: ops.Framework):
        super().__init__(framework)
        framework.observe(self.on.start, self._on_start)
        framework.observe(self.on.set_action, self._on_set)

    def _on_start(self, event: ops.StartEvent):
        self.unit.add_secret(label="my-secret", content={"foo": "bar"})
        self.unit.status = ops.ActiveStatus()

    def _on_set(self, event):
        secret = self.model.get_secret(label="my-secret")
        secret.set_content(event.params)
        secret.set_info(description="hello world")

If you call the action (juju run unit-name/0 set one=two) with the code like it is above, then the description will be set, but the content will not. If you swap the code so that the set_content and set_info calls are around the other way, then the action will set the content but not the description.

@tonyandrewmeyer tonyandrewmeyer added the bug Something isn't working label Jul 18, 2024
@tonyandrewmeyer
Copy link
Contributor Author

This seems non-trivial to fix - ops would need to keep the data passed to both set_content and set_info in a cache (merging as each was called) and then do the secret_set call only on conclusion of the hook (note that it's not just the current event, as the calls might be in the Juju event but also in a deferred or ops Lifecycle event).

At minimum, we should document this restriction so that it's easier to figure out what's happening.

@benhoyt
Copy link
Collaborator

benhoyt commented Jul 18, 2024

Thanks for the report. It does seem odd to me that the second secret-set call (without content args) wipes out the content, so maybe this is a Juju bug. I'll put it on the list to discuss with Ian next Monday.

@benhoyt
Copy link
Collaborator

benhoyt commented Jul 22, 2024

We discussed this in daily sync today, and decided this is probably is worth fixing. You'd be likely to call set_info when a secret expires, and you'd want to call set_content then too. So this could really happen in practice.

Our idea is to keep a cache on the backend (dict of secret id to something), and whenever set_info or set_content is called, it updates the info, merging info and content. But setting content a second time would replace.

@benhoyt benhoyt added the 24.10 label Jul 22, 2024
@tonyandrewmeyer
Copy link
Contributor Author

Interestingly, the tls-certificates-interface lib does have a set_content() followed immediately by set_info() in relation-changed. It seems like that can't work at the moment - there is another path where the secret doesn't already exist and a new one is created - maybe that is the one that always gets exercised?

It would be good if whoever works on this ticket takes a closer look at that, before and after the change.

@saltiyazan
Copy link

Interestingly, the tls-certificates-interface lib does have a set_content() followed immediately by set_info() in relation-changed. It seems like that can't work at the moment - there is another path where the secret doesn't already exist and a new one is created - maybe that is the one that always gets exercised?

It would be good if whoever works on this ticket takes a closer look at that, before and after the change.

I looked into this, and it appears to be dead code that will no longer be executed due to some changes introduced to the library. I'll investigate this further to confirm.

@saltiyazan
Copy link

I came across this issue and I was about to open a bug, happy that you are aware if it.
Do you suggest a specific workaround until a fix is merged?

@tonyandrewmeyer
Copy link
Contributor Author

tonyandrewmeyer commented Sep 16, 2024

I came across this issue and I was about to open a bug, happy that you are aware if it.

Thanks for letting us know - I'll see if we can get a fix in for the next release.

Do you suggest a specific workaround until a fix is merged?

Unfortunately, I think the only workaround would be to call the hook tool yourself - basically, add a method to your charm that does what set_info does but can also set the content, using the private attributes to get the model backend, and use that.

@tonyandrewmeyer tonyandrewmeyer self-assigned this Sep 16, 2024
tonyandrewmeyer added a commit that referenced this issue Sep 25, 2024
… hook (#1373)

Each call to `secret-set` replaces the values in any previous
`secret-set` call in the same hook. Since both `Secret.set_content` and
`Secret.set_info` use `secret-set`, this means that doing both in the
same hook would only retain the change from whichever was done last.

This PR changes the model backend to cache the secret metadata (from
`set_info`) and add it into any subsequent `secret-set` calls. Although
the metadata is only available to the secret owner, it does not seem so
private that it is unsafe to keep it in memory for the duration of the
hook execution.

There is an additional behaviour change here: previously calling
`set_info` twice in the same hook would 'undo' setting metadata. For
example:

```python
secret.set_info(label="foo")
secret.set_info(description="bar")
# With main, the secret will end up with a description and no label
# With the PR branch, the secret will end up with a description and a label
```

I believe this is a bug fix also, because it seems likely that a charmer
would expect both values to be set with code such as the above, and the
documentation states:

> Once attributes are set, they cannot be unset.

For the secret content, I believe we should not cache this in the charm
process's memory, so this PR only sets a sentinel that the content has
been set, and if there is a subsequent `set_info` call, the content is
retrieved via a `secret-get` call (I believe this is from the in-memory
cache in the unit agent, but, in any case, it's the un-committed value
in an existing location, so does not increase the secret content
exposure).

There is an example charm in #1288 that can be used to verify the
behaviour in a real Juju model.

No updates to Harness because it already has the behaviour that we're
changing to.

Fixes #1288
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
24.10 bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants