-
Notifications
You must be signed in to change notification settings - Fork 119
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
feat: parse JUJU_* env in one place #1313
Conversation
chore: poc parsing juju env in one place chore: refactor _JuJuContext to a dataclass
For what it's worth, I agree that it's not worth grouping (flat is better than nested). However, I do think it's worth doing the type conversion in the class, as well as any I'd also suggest that the |
Consider
If we're going to cast or interpret the values, we gotta handle these special cases too. In this case, I think |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's finalise the decision of what approach to use in the daily sync today.
Note that there are also a bunch of uses of os.environ
and os.getenv
is other files:
framework.py
655: debug_at = os.environ.get('JUJU_DEBUG_AT')
jujuversion.py
104: v = os.environ.get('JUJU_VERSION')
model.py
3234: if 'JUJU_RELATION_ID' in os.environ and 'JUJU_REMOTE_APP' in os.environ:
3235: event_relation_id = int(os.environ['JUJU_RELATION_ID'].split(':')[-1])
model.py
3161: unit_name_ = unit_name or os.getenv('JUJU_UNIT_NAME')
3167: self.model_name: str = model_name or typing.cast(str, os.getenv('JUJU_MODEL_NAME'))
3168: self.model_uuid: str = model_uuid or typing.cast(str, os.getenv('JUJU_MODEL_UUID'))
3238: return os.getenv('JUJU_REMOTE_APP') or None
After discussion, we will use a flat structure without nested objects, and we will put all fields as |
Notes:
Changes:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like a great start, thanks. A few initial comments.
db-charm-tests failed because the charm imports JujuVersion from |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good! A few small comments.
I tried this out on Juju 3.6, LXD, but only with a few events, not exhaustively. Everything looked good for the ones I tried.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks really good. Just a couple more requests for file moving (sorry for the back and forth!).
Decision was to put JUJU_DEBUG_AT in |
… into parse-juju-env-vars
All comments resolved. Major changes:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So happy to see this PR!
""" | ||
|
||
@classmethod | ||
def from_dict(cls, env: Mapping[str, Any]) -> '_JujuContext': |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Design question:
The half-baked idea I imagined was that from_dict
returns a Union
of types, and the caller always uses a match expression on the result. It would imply py3.10 (ubuntu 22.04) which isn't great, but I wonder (a) if it's a feasible design goal in the first place, and (b) if we should plan the design today with match
in mind.
I.e. it would be great if this new class brought significant new added value over poking into os.environ
directly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Example: https://stackoverflow.com/a/71519690
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understand correctly, there is already some similar logic in main.py
, specifically, in _get_event_args
, where it tries to match the type of the event then pull related environment variables.
I think it would be great if we could somehow integrate it with JujuContext, maybe in the future we can build on top of this PR and implement that.
Regarding the scope of this PR, we had a short discussion today in the daily and we think this PR serves two purposes: 1, parse all ENV vars from a single place (previously it was scattered across main.py
and framework.py
); and 2, provide some unified object so that others can build on top of it if they want to implement some experiments.
So, for now, I'm sorry that there is no implementation of the event type matching thingy and we are going to merge this _JujuContext
. Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The half-baked idea I imagined was that
from_dict
returns aUnion
of types, and the caller always uses a match expression on the result.
If the intention is to find out which event Juju has emitted, then it's much simpler to just look at that directly rather than trying to match against which context variables have been set. If the intention is to get back a collection of types (e.g. which fields are set are the same with different events - start and stop would be setting the same) then I'm not sure I really see the use-case.
I don't think it's good to have a hierarchy of context objects when there's already a 1:1 mapping of what that would be in the hierarchy of event classes. A SecretChangedJujuContext
would only be useful for SecretChangedEvent
, for example.
As @IronCore864 mentioned, _get_event_args
provides the "given a context, what are the arguments needed to create an event object". It would be simple enough for someone to subclass _JujuContext
to add a to_event
method (or whatever), if they wanted to build an "alt ops" package.
What would be most useful in that sort of situation, I believe, is being able to create SecretChangedEvent
(and on on) objects without opting in to the whole framework/handle system (and registering, in particular). My feeling is that's what we could provide (not this cycle) to continue making it easier for people to explore alternative ways to do event handling without re-implementing everything.
I.e. it would be great if this new class brought significant new added value over poking into
os.environ
directly.
"significant" is subjective, of course, but I think there is value in not just collecting all the uses together as @IronCore864 mentioned, but also doing all the conversion so that you have a bunch of Python objects rather than a bunch of strings. With this, plus the similar hook tool wrapping in the model/model backend (the border of those could be a bit cleaner) that's the main interface with Juju (other than Pebble).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great!
I'm fine with this as-is, but left a couple of small suggestions - feel free to discard those if you prefer.
The next release of ops has some backwards-incompatible changes to private methods. This PR does the minimum to keep Scenario working with the current version of ops and the next release. I'll open a ticket for doing a nicer version of this where the new `_JujuContext` class is used (which would presumably mean requiring the new version of ops). But this will let people continue upgrading their ops as long as they're using the latest 6.x of Scenario. The relevant ops PR is: canonical/operator#1313
As of canonical/operator#1313 , the test harness inserts JUJU_VERSION=0.0.0 into os.environ, so we must account for this in the unit tests.
if 'JUJU_VERSION' not in os.environ: | ||
os.environ['JUJU_VERSION'] = '0.0.0' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@IronCore864 why does the test framework need to manipulate the environment now? The new context appears to already pick a default if JUJU_VERSION is not present.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@IronCore864 why does the test framework need to manipulate the environment now? The new context appears to already pick a default if JUJU_VERSION is not present.
Note that this is the internal tests for ops themselves, not ops.testing (Harness). It seems like no-one else should really care what the internal ops tests are doing. Does this actually impact you, or is it just something you noticed while running the ops tests or similar?
It's being set here because _JujuContext
expects the environment variable to be set, because all supported Juju versions so do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this actually impact you, or is it just something you noticed while running the ops tests or similar?
Yes, it's affecting our unit tests that are checking the arguments that were called in subprocess. See an example at canonical/charm-simple-streams#22
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, it's not the code linked above (your tests will never run that), it's this similar code in ops.testing.
We failed to consider this 😞, and we must not have run this change against the full set of charms we know about 😞. It would indeed be better to have Harness not change the environment. It's not immediately clear to me what we should do now: we could change the behaviour back so that it doesn't, but if everyone that this impacts has then already changed their code by the time that fix gets out, then it's just doubling up the impact (although it is cleaner in general).
I'll talk with the team about this later today.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI, we're going to do an ops 2.16.1 change that reverts this behaviour.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I confirm that now it's fixed 👍
Parse all
JUJU_*
env vars in one place.Closes #1075.
Note: This draft does the minimum changes by creating a
dataclass
, no other logic inmain.py
is changed. Variables are not grouped into smallerdataclasses
, and all types are string just as they were inos.environ.get()
.There will be another alternative draft with variable grouping and typing.
More Explanations
1 Grouping
I think it's fine without it but I'm not against it either. If there is a solid reason for grouping, I'm OK with it, but here are my thoughts:
JUJU_NOTICE_*
andJUJU_SECRET_*
can be grouped into a smaller dataclass. Maybe we can argue thatJUJU_RELATION
andJUJU_RELATION_ID
can be grouped too, but then the first needs to be renamed toJUJU_RELATION_NAME
which deviates from the acutal env variable name. So, most env vars can't really be grouped, which means the_JujuContext
dataclass can't be shortened or improved readability-wise by grouping. And when using it, it's not too much difference, comparingjuju_context_obj.juju_notice_id
andjuju_context_obj.juju_notice.id
. So I think the value added by grouping is insignificant._JujuContext.notice.id
and_JujuContext.juju_charm_dir
, it might indicate that_JujuContext
ownsnotice
andjuju_charm_dir
, they are on the same level, and there is another object that isnotice
who ownsid
, while in fact it's not the case, they are all env vars.2 Type
I put all fields as str in this draft, because:
JUJU_SECRET_REVISION
; and some values are processed based on the env var, like juju relation id. So, maybe it's easier to keep them as they are and handle the type conversion whenever needed (mostly aren't). There are on extra plus side and that is we don't need to do extra processing if the key isJUJU_SECRET_REVISION
, keeping thefrom_env
method simple without key-name related logic._JujuContext
dataclass comes from env vars, it's intuitive to think all fields are strings.Some explanation on
Optional
or not, and default values, becaues it may seem strange at first glace, and these logic is not from Juju but fromops.main
, how we are using them currently:Optional
: Some fields are notOptional
because inmain.py
the current usage of them is treating them like they surely exist (os.environ[key]
), so I kept them as typestr
instead ofOptional[str]
.Optional
with None value because in the current logic there are tests likeif key in os.environ
.None
so I left these type all asOptional
that can beNone
.