-
Notifications
You must be signed in to change notification settings - Fork 14
Relation data wrapper #82
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
Open
PietroPasotti
wants to merge
12
commits into
charmed-kubernetes:main
Choose a base branch
from
PietroPasotti:relation-data-wrapper
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 7 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
cd3a9e4
ffwd util
PietroPasotti ba08046
linted, test ok
PietroPasotti 27cfb35
updated reference
PietroPasotti f3a8b57
relation data wrapper
PietroPasotti 5cd53e9
linted
PietroPasotti 5304aa4
merged from main
PietroPasotti 7f6e3f1
popped idea
PietroPasotti 91cc71c
Merge branch 'main' into relation-data-wrapper
731c32c
Resolve Lint issue
d5e2f09
Include Integration tests of this feature
f23d930
Update for Linting
cae80ef
remove debug code
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| import asyncio | ||
| from dataclasses import dataclass | ||
| from subprocess import PIPE, Popen | ||
| from typing import Dict | ||
|
|
||
| import yaml | ||
|
|
||
| _JUJU_DATA_CACHE = {} | ||
| _JUJU_KEYS = ("egress-subnets", "ingress-address", "private-address") | ||
|
|
||
|
|
||
| def _purge(data: dict): | ||
| for key in _JUJU_KEYS: | ||
| if key in data: | ||
| del data[key] | ||
|
|
||
|
|
||
| async def _get_unit_info(unit_name: str) -> dict: | ||
| """Returns unit-info data structure. | ||
|
|
||
| for example: | ||
|
|
||
| traefik-k8s/0: | ||
| opened-ports: [] | ||
| charm: local:focal/traefik-k8s-1 | ||
| leader: true | ||
| relation-info: | ||
| - endpoint: ingress-per-unit | ||
| related-endpoint: ingress | ||
| application-data: | ||
| _supported_versions: '- v1' | ||
| related-units: | ||
| prometheus-k8s/0: | ||
| in-scope: true | ||
| data: | ||
| egress-subnets: 10.152.183.150/32 | ||
| ingress-address: 10.152.183.150 | ||
| private-address: 10.152.183.150 | ||
| provider-id: traefik-k8s-0 | ||
| address: 10.1.232.144 | ||
| """ | ||
| if cached_data := _JUJU_DATA_CACHE.get(unit_name): | ||
| return cached_data | ||
|
|
||
| proc = Popen(f"juju show-unit {unit_name}".split(" "), stdout=PIPE) | ||
| raw_data = proc.stdout.read().decode("utf-8").strip() | ||
| if not raw_data: | ||
| raise ValueError( | ||
| f"no unit info could be grabbed for {unit_name}; " | ||
| f"are you sure it's a valid unit name?" | ||
| ) | ||
|
|
||
| data = yaml.safe_load(raw_data) | ||
| _JUJU_DATA_CACHE[unit_name] = data | ||
| return data | ||
|
|
||
|
|
||
| def _get_relation_by_endpoint(relations, endpoint, remote_obj): | ||
| relations = [ | ||
| r | ||
| for r in relations | ||
| if r["endpoint"] == endpoint and remote_obj in r["related-units"] | ||
| ] | ||
| if not relations: | ||
| raise ValueError(f"no relations found with endpoint==" f"{endpoint}") | ||
| if len(relations) > 1: | ||
| raise ValueError("multiple relations found with endpoint==" f"{endpoint}") | ||
| return relations[0] | ||
|
|
||
|
|
||
| @dataclass | ||
| class UnitRelationData: | ||
| unit_name: str | ||
| endpoint: str | ||
| leader: bool | ||
| application_data: Dict[str, str] | ||
| unit_data: Dict[str, str] | ||
|
|
||
|
|
||
| async def _get_endpoint_content( | ||
| obj: str, other_obj, include_default_juju_keys: bool = False | ||
| ) -> UnitRelationData: | ||
| """Get the content of the databag of `obj`, relative to `other_obj`.""" | ||
| endpoint = None | ||
| other_unit_name = other_obj.split(":")[0] if ":" in other_obj else other_obj | ||
| if ":" in obj: | ||
| unit_name, endpoint = obj.split(":") | ||
| else: | ||
| unit_name = obj | ||
| data = (await _get_unit_info(unit_name))[unit_name] | ||
| is_leader = data["leader"] | ||
|
|
||
| relation_infos = data.get("relation-info") | ||
| if not relation_infos: | ||
| raise RuntimeError(f"{unit_name} has no relations") | ||
|
|
||
| if not endpoint: | ||
| relation_data_raw = relation_infos[0] | ||
| endpoint = relation_data_raw["endpoint"] | ||
| else: | ||
| relation_data_raw = _get_relation_by_endpoint( | ||
| relation_infos, endpoint, other_unit_name | ||
| ) | ||
|
|
||
| related_units_data_raw = relation_data_raw["related-units"] | ||
|
|
||
| other_unit_name = next(iter(related_units_data_raw.keys())) | ||
| other_unit_info = await _get_unit_info(other_unit_name) | ||
| other_unit_relation_infos = other_unit_info[other_unit_name]["relation-info"] | ||
| remote_data_raw = _get_relation_by_endpoint( | ||
| other_unit_relation_infos, relation_data_raw["related-endpoint"], unit_name | ||
| ) | ||
| this_unit_data = remote_data_raw["related-units"][unit_name]["data"] | ||
| this_app_data = remote_data_raw["application-data"] | ||
|
|
||
| if not include_default_juju_keys: | ||
| _purge(this_unit_data) | ||
|
|
||
| return UnitRelationData( | ||
| unit_name, endpoint, is_leader, this_app_data, this_unit_data | ||
| ) | ||
|
|
||
|
|
||
| @dataclass | ||
| class RelationData: | ||
| provider: UnitRelationData | ||
| requirer: UnitRelationData | ||
|
|
||
|
|
||
| async def get_relation_data( | ||
| provider_endpoint: str, requirer_endpoint: str, include_juju_keys: bool = False | ||
| ) -> RelationData: | ||
| """Get relation databag contents for both sides of a juju relation. | ||
|
|
||
| Usage: | ||
| >>> data: RelationData = await ops_test.get_relation_data( | ||
| ... 'prometheus/0:ingress', 'traefik/1:ingress-per-unit') | ||
| >>> assert data.provider.application_data['key'] = 'foo' | ||
| """ | ||
| provider_data, requirer_data = await asyncio.gather( | ||
| _get_endpoint_content(provider_endpoint, requirer_endpoint, include_juju_keys), | ||
| _get_endpoint_content(requirer_endpoint, provider_endpoint, include_juju_keys), | ||
| ) | ||
| return RelationData(provider_data, requirer_data) | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 anywhere else use the provider endpoint like this as an argument? I can't think of any, but I probably have forgotten or never known in the first place. I ask just because on first read this felt odd to see in charm code, but it might just be because I haven't dealt with similar things
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.
Yeah so this is my own take on the matter. Not sure how else to expose this.
Issue is: we need an app name, a unit number, and a endpoint name. It felt that maintaining the 'juju cli' syntax was the least disruptive option, so
unit_name/unit_number:endpoint_name.But I'd be willing to unpack this into a more semantically expressive object, a NamedTuple('unit, number, endpoint') or something like that. Or maybe we should be accepting juju.model.Unit etc... but I wanted to avoid forcing the user to fetch a specific unit instance when the app name is usually much easier to get in itests...