-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* fixed dependencies * moved cluster over to new dir * ds exchange interface implementation * typing * add datasource exchange to coordinator * utests * fmt * made the endpoints optional * pr comments
- Loading branch information
1 parent
eedfe50
commit 8ae974b
Showing
8 changed files
with
407 additions
and
27 deletions.
There are no files selected for viewing
This file contains 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 |
---|---|---|
|
@@ -4,21 +4,23 @@ build-backend = "hatchling.build" | |
|
||
[project] | ||
name = "cosl" | ||
version = "0.0.44" | ||
version = "0.0.45" | ||
authors = [ | ||
{ name="sed-i", email="[email protected]" }, | ||
{ name = "sed-i", email = "[email protected]" }, | ||
] | ||
description = "Utils for COS Lite charms" | ||
readme = "README.md" | ||
license = {file = "LICENSE"} | ||
license = { file = "LICENSE" } | ||
requires-python = ">=3.8" | ||
dependencies = [ | ||
"ops", | ||
"pydantic", | ||
"tenacity", | ||
"PyYAML", | ||
"typing-extensions", | ||
"lightkube>=v0.15.4" | ||
"ops", | ||
"pydantic", | ||
"tenacity", | ||
"jsonschema", | ||
"PyYAML", | ||
"typing-extensions", | ||
"lightkube>=v0.15.4", | ||
"charm-relation-interfaces @ git+https://github.com/canonical/charm-relation-interfaces", | ||
] | ||
classifiers = [ | ||
"Programming Language :: Python :: 3.8", | ||
|
@@ -60,21 +62,21 @@ extend-exclude = ["__pycache__", "*.egg_info"] | |
[tool.ruff.lint] | ||
select = ["E", "W", "F", "C", "N", "D", "I001"] | ||
extend-ignore = [ | ||
"D203", | ||
"D204", | ||
"D213", | ||
"D215", | ||
"D400", | ||
"D404", | ||
"D406", | ||
"D407", | ||
"D408", | ||
"D409", | ||
"D413", | ||
"E402", | ||
"D203", | ||
"D204", | ||
"D213", | ||
"D215", | ||
"D400", | ||
"D404", | ||
"D406", | ||
"D407", | ||
"D408", | ||
"D409", | ||
"D413", | ||
"E402", | ||
] | ||
ignore = ["E501", "D107"] | ||
per-file-ignores = {"tests/*" = ["D100","D101","D102","D103","D104"]} | ||
per-file-ignores = { "tests/*" = ["D100", "D101", "D102", "D103", "D104"] } | ||
|
||
[tool.ruff.lint.pydocstyle] | ||
convention = "google" | ||
|
@@ -88,6 +90,7 @@ classmethod-decorators = ["classmethod", "pydantic.validator"] | |
|
||
[tool.pyright] | ||
include = ["src"] | ||
|
||
extraPaths = ["lib", "src/cosl"] | ||
pythonVersion = "3.8" | ||
pythonPlatform = "All" | ||
|
@@ -99,3 +102,8 @@ reportTypeCommentUsage = false | |
[tool.codespell] | ||
skip = ".git,.tox,build,lib,venv*,.mypy_cache" | ||
ignore-words-list = "assertIn" | ||
|
||
[tool.hatch.metadata] | ||
# allow git+ dependencies in pyproject | ||
allow-direct-references = true | ||
|
This file contains 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 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 @@ | ||
#!/usr/bin/env python3 | ||
# Copyright 2024 Canonical | ||
# See LICENSE file for licensing details. | ||
|
||
"""Shared utilities for the inter-coordinator "grafana_datasource_exchange" interface. | ||
See https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/grafana_datasource_exchange/v0 | ||
for the interface specification. | ||
""" | ||
|
||
|
||
# FIXME: the interfaces import (because it's a git dep perhaps?) | ||
# can't be type-checked, which breaks everything | ||
# pyright: reportMissingImports=false | ||
# pyright: reportUntypedBaseClass=false | ||
# pyright: reportUnknownLambdaType=false | ||
# pyright: reportUnknownMemberType=false | ||
# pyright: reportUnknownVariableType=false | ||
# pyright: reportUnknownArgumentType=false | ||
# pyright: reportUnknownParameterType=false | ||
|
||
|
||
import json | ||
import logging | ||
from typing import ( | ||
Iterable, | ||
List, | ||
Optional, | ||
Tuple, | ||
) | ||
|
||
import ops | ||
from interfaces.grafana_datasource_exchange.v0.schema import ( | ||
GrafanaDatasource, | ||
GrafanaSourceAppData, | ||
) | ||
from ops import CharmBase | ||
from typing_extensions import TypedDict | ||
|
||
import cosl.interfaces.utils | ||
from cosl.interfaces.utils import DataValidationError | ||
|
||
log = logging.getLogger("datasource_exchange") | ||
|
||
DS_EXCHANGE_INTERFACE_NAME = "grafana_datasource_exchange" | ||
|
||
|
||
class DSExchangeAppData(cosl.interfaces.utils.DatabagModelV2, GrafanaSourceAppData): | ||
"""App databag schema for both sides of this interface.""" | ||
|
||
|
||
class DatasourceDict(TypedDict): | ||
"""Raw datasource information.""" | ||
|
||
type: str | ||
uid: str | ||
grafana_uid: str | ||
|
||
|
||
class EndpointValidationError(ValueError): | ||
"""Raised if an endpoint name is invalid.""" | ||
|
||
|
||
def _validate_endpoints( | ||
charm: CharmBase, provider_endpoint: Optional[str], requirer_endpoint: Optional[str] | ||
): | ||
meta = charm.meta | ||
for endpoint, source in ( | ||
(provider_endpoint, meta.provides), | ||
(requirer_endpoint, meta.requires), | ||
): | ||
if endpoint is None: | ||
continue | ||
if endpoint not in source: | ||
raise EndpointValidationError(f"endpoint {endpoint!r} not declared in charm metadata") | ||
interface_name = source[endpoint].interface_name | ||
if interface_name != DS_EXCHANGE_INTERFACE_NAME: | ||
raise EndpointValidationError( | ||
f"endpoint {endpoint} has unexpected interface {interface_name!r} " | ||
f"(should be {DS_EXCHANGE_INTERFACE_NAME})." | ||
) | ||
if not provider_endpoint and not requirer_endpoint: | ||
raise EndpointValidationError( | ||
"This charm should implement either a requirer or a provider (or both)" | ||
"endpoint for `grafana-datasource-exchange`." | ||
) | ||
|
||
|
||
class DatasourceExchange: | ||
"""``grafana_datasource_exchange`` interface endpoint wrapper (provider AND requirer).""" | ||
|
||
def __init__( | ||
self, | ||
charm: ops.CharmBase, | ||
*, | ||
provider_endpoint: Optional[str], | ||
requirer_endpoint: Optional[str], | ||
): | ||
self._charm = charm | ||
_validate_endpoints(charm, provider_endpoint, requirer_endpoint) | ||
|
||
# gather all relations, provider or requirer | ||
all_relations = [] | ||
if provider_endpoint: | ||
all_relations.extend(charm.model.relations.get(provider_endpoint, ())) | ||
if requirer_endpoint: | ||
all_relations.extend(charm.model.relations.get(requirer_endpoint, ())) | ||
|
||
# filter out some common unhappy relation states | ||
self._relations: List[ops.Relation] = [ | ||
rel for rel in all_relations if (rel.app and rel.data) | ||
] | ||
|
||
def publish(self, datasources: Iterable[DatasourceDict]): | ||
"""Submit these datasources to all remotes. | ||
This operation is leader-only. | ||
""" | ||
# sort by UID to prevent endless relation-changed cascades if this keeps flapping | ||
encoded_datasources = json.dumps(sorted(datasources, key=lambda raw_ds: raw_ds["uid"])) | ||
app_data = DSExchangeAppData( | ||
datasources=encoded_datasources # type: ignore[reportCallIssue] | ||
) | ||
|
||
for relation in self._relations: | ||
app_data.dump(relation.data[self._charm.app]) | ||
|
||
@property | ||
def received_datasources(self) -> Tuple[GrafanaDatasource, ...]: | ||
"""Collect all datasources that the remotes have shared. | ||
This operation is leader-only. | ||
""" | ||
datasources: List[GrafanaDatasource] = [] | ||
|
||
for relation in self._relations: | ||
try: | ||
datasource = DSExchangeAppData.load(relation.data[relation.app]) | ||
except DataValidationError: | ||
# load() already logs something in this case | ||
continue | ||
|
||
datasources.extend(datasource.datasources) | ||
return tuple(sorted(datasources, key=lambda ds: ds.uid)) |
This file contains 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 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 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
Oops, something went wrong.